diff --git a/src/main.ts b/src/main.ts index a42fd96..dd5964c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,7 +39,8 @@ import { getBookmarksPlugin, bookmarkFolderItem, saveDataAndUpdateBookmarkViews, - bookmarkSiblings + bookmarkSiblings, + updateSortingBookmarksAfterItemRename } from "./utils/BookmarksCorePluginSignature"; import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature"; import {lastPathComponent} from "./utils/utils"; @@ -354,6 +355,15 @@ export default class CustomSortPlugin extends Plugin { menu.addItem(bookmarkAllMenuItem) }) ) + + this.registerEvent( + this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => { + const bookmarksPlugin = getBookmarksPlugin(plugin.app) + if (bookmarksPlugin) { + updateSortingBookmarksAfterItemRename(bookmarksPlugin, file, oldPath, plugin.settings.bookmarksGroupToConsumeAsOrderingReference) + } + }) + ) } registerCommands() { @@ -628,3 +638,5 @@ class CustomSortSettingTab extends PluginSettingTab { // Better the plugin to fail an operation than crash with errors // TODO: remove console.log (many places added) + +// Invoke 'onItemsChanged' consciously for the bookmarks plugin diff --git a/src/utils/BookmarksCorePluginSignature.ts b/src/utils/BookmarksCorePluginSignature.ts index 394670a..6459df0 100644 --- a/src/utils/BookmarksCorePluginSignature.ts +++ b/src/utils/BookmarksCorePluginSignature.ts @@ -1,5 +1,5 @@ import {App, InstalledPlugin, Plugin, PluginInstance, TAbstractFile, TFolder} from "obsidian"; -import {lastPathComponent} from "./utils"; +import {extractParentFolderPath, lastPathComponent} from "./utils"; const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks' @@ -56,6 +56,7 @@ export interface Bookmarks_PluginInstance extends PluginInstance { [BookmarksPlugin_getBookmarks_methodName]: () => Array | undefined [BookmarksPlugin_items_collectionName]: Array saveData(): void + onItemsChanged(saveData: boolean): void } let bookmarksCache: OrderedBookmarks | undefined = undefined @@ -108,6 +109,8 @@ const traverseBookmarksCollection = (items: Array, callback: Tra recursiveTraversal(items, ''); } +const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^.' + const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): OrderedBookmarks | undefined => { console.log(`Populating bookmarks cache with group scope ${bookmarksGroupName}`) let bookmarks: Array | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]() @@ -122,10 +125,10 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupNam let order: number = 0 const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => { const isFile: boolean = item.type === 'file' - const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath + const hasSortspecAnchor: boolean = isFile && (item as BookmarkedFile).subpath === ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR const isFolder: boolean = item.type === 'folder' const isGroup: boolean = item.type === 'group' - if ((isFile && !isAnchor) || isFolder || isGroup) { + if ((isFile && hasSortspecAnchor) || isFolder || isGroup) { const pathOfGroup: string = `${parentGroupsPath}${parentGroupsPath?'/':''}${item.title}` const path = isGroup ? pathOfGroup : (item as BookmarkWithPath).path // Consume only the first occurrence of a path in bookmarks, even if many duplicates can exist @@ -165,7 +168,9 @@ export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginIns // EXPERIMENTAL - operates on internal structures of core Bookmarks plugin const createBookmarkFileEntry = (path: string): BookmarkedFile => { - return { type: "file", ctime: Date.now(), path: path } + // Artificial subpath added intentionally to prevent Bookmarks context menu from finding this item in bookmarks + // and - in turn - allow bookmarking it by the user for regular (non sorting) purposes + return { type: "file", ctime: Date.now(), path: path, subpath: ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR } } const createBookmarkGroupEntry = (title: string): BookmarkedGroup => { @@ -176,42 +181,127 @@ export const bookmarkFolderItem = (item: TAbstractFile, plugin: Bookmarks_Plugin bookmarkSiblings([item], plugin, bookmarksGroup) } -export const bookmarkSiblings = (siblings: Array, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => { +interface BookmarkedParentFolder { + group?: BookmarkedGroup // undefined when the item is at root level of bookmarks + items: Array // reference to group.items or to root collection of bookmarks +} + +interface ItemInBookmarks { + parentItemsCollection: Array + item: BookmarkedItem +} + +const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: boolean, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): BookmarkedParentFolder|undefined => { let items = plugin[BookmarksPlugin_items_collectionName] - if (siblings.length === 0) return // for sanity + if (!itemPath || !itemPath.trim()) return undefined // for sanity - const parentPathComponents: Array = siblings[0].path.split('/')! - parentPathComponents.pop() + const parentPath: string = extractParentFolderPath(itemPath) + + const parentPathComponents: Array = parentPath ? parentPath.split('/')! : [] if (bookmarksGroup) { parentPathComponents.unshift(bookmarksGroup) } + let group: BookmarkedGroup|undefined = undefined + parentPathComponents.forEach((pathSegment) => { let group: BookmarkedGroup|undefined = items.find((it) => it.type === 'group' && it.title === pathSegment) as BookmarkedGroup if (!group) { - group = createBookmarkGroupEntry(pathSegment) - items.push(group) + if (createIfMissing) { + group = createBookmarkGroupEntry(pathSegment) + items.push(group) + } else { + return undefined + } } + items = group.items }) - siblings.forEach((aSibling) => { - const siblingName = lastPathComponent(aSibling.path) - if (!items.find((it) => - ((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) || - (it.type === 'group' && it.title === siblingName))) { - const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path) - items.push(newEntry) - } - }); + return { + items: items, + group: group + } +} + +const CreateIfMissing = true +const DontCreateIfMissing = false + +export const bookmarkSiblings = (siblings: Array, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => { + if (siblings.length === 0) return // for sanity + + const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(siblings[0].path, CreateIfMissing, plugin, bookmarksGroup) + + if (bookmarksContainer) { // for sanity, the group should be always created if missing + siblings.forEach((aSibling) => { + const siblingName = lastPathComponent(aSibling.path) + if (!bookmarksContainer.items.find((it) => + ((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) || + (it.type === 'group' && it.title === siblingName))) { + const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path) + bookmarksContainer.items.push(newEntry) + } + }); + } } export const saveDataAndUpdateBookmarkViews = (plugin: Bookmarks_PluginInstance, app: App) => { - plugin.saveData() + plugin.onItemsChanged(true) const bookmarksLeafs = app.workspace.getLeavesOfType('bookmarks') bookmarksLeafs?.forEach((leaf) => { (leaf.view as any)?.update?.() }) } + +export const updateSortingBookmarksAfterItemRename = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => { + + if (renamedItem.path === oldPath) return; // sanity + + let items = plugin[BookmarksPlugin_items_collectionName] + const aFolder: boolean = renamedItem instanceof TFolder + const aFolderWithChildren: boolean = aFolder && (renamedItem as TFolder).children.length > 0 + const aFile: boolean = !aFolder + const oldParentPath: string = extractParentFolderPath(oldPath) + const oldName: string = lastPathComponent(oldPath) + const newParentPath: string = extractParentFolderPath(renamedItem.path) + const newName: string = lastPathComponent(renamedItem.path) + const moved: boolean = oldParentPath !== newParentPath + const renamed: boolean = oldName !== newName + + const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(oldPath, DontCreateIfMissing, plugin, bookmarksGroup) + + if (!originalContainer) return; + + const item: BookmarkedItem|undefined = originalContainer.items.find((it) => { + if (aFolder && it.type === 'group' && it.title === oldName) return true; + if (aFile && it.type === 'file' && it.path === oldPath) return true; + }) + + if (!item) return; + + // The renamed/moved item was located in bookmarks, apply the necessary bookmarks updates + + let itemRemovedFromBookmarks: boolean = false + if (moved) { + originalContainer.items.remove(item) + const createTargetLocation: boolean = aFolderWithChildren + const targetContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(renamedItem.path, createTargetLocation, plugin, bookmarksGroup) + if (targetContainer) { + targetContainer.items.push(item) + } else { + itemRemovedFromBookmarks = true // open question: remove from bookmarks indeed, if target location was not under bookmarks control? + } + } + + if (aFolder && renamed && !itemRemovedFromBookmarks) { + // Renames of files are handled automatically by Bookmarks core plugin, only need to handle folder rename + // because folders are represented (for sorting purposes) by groups with exact name + (item as BookmarkedGroup).title = newName + } + + console.log(`Folder renamel from ${oldPath} to ${renamedItem.path}`) + + plugin.onItemsChanged(true) +} diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts new file mode 100644 index 0000000..5189d85 --- /dev/null +++ b/src/utils/utils.spec.ts @@ -0,0 +1,24 @@ +import {lastPathComponent, extractParentFolderPath} from "./utils"; + +describe('lastPathComponent and extractParentFolderPath', () => { + it.each([ + ['a folder', '', 'a folder'], + ['a/subfolder', 'a', 'subfolder'], + ['parent/child', 'parent', 'child'], + ['','',''], + [' ','',''], + ['/strange', '', 'strange'], + ['a/b/c/', 'a/b/c', ''], + ['d d d/e e e/f f f/ggg ggg', 'd d d/e e e/f f f', 'ggg ggg'], + ['/','',''], + [' / ','',''], + [' /','',''], + ['/ ','',''] + ])('should from %s extract %s and %s', (path: string, parentPath: string, lastComponent: string) => { + const extractedParentPath: string = extractParentFolderPath(path) + const extractedLastComponent: string = lastPathComponent(path) + expect(extractedParentPath).toBe(parentPath) + expect(extractedLastComponent).toBe(lastComponent) + } + ) +}) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index e833cbf..5afece4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -8,6 +8,11 @@ export function last(o: Array): T | undefined { } export function lastPathComponent(path: string): string { - const pathComponents = (path ?? '').split('/') - return pathComponents.pop()! + const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/') + return lastPathSeparatorIdx >= 0 ? path.substring(lastPathSeparatorIdx + 1).trim() : path.trim() +} + +export function extractParentFolderPath(path: string): string { + const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/') + return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx).trim() : '' }