From a07f55a037dc4b621651ec313ac43fb851e8b81c Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:08:24 +0200 Subject: [PATCH] #74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement - completed! - need to see if additional unit tests can be created --- .../custom-sort-getComparator.spec.ts | 2 - src/custom-sort/custom-sort.spec.ts | 17 +++- src/custom-sort/folder-matching-rules.spec.ts | 4 +- src/custom-sort/macros.spec.ts | 10 +- src/custom-sort/matchers.spec.ts | 1 - .../sorting-spec-processor.spec.ts | 12 ++- src/main.ts | 40 ++------ src/utils/BookmarksCorePluginSignature.ts | 91 +++++++++++-------- 8 files changed, 96 insertions(+), 81 deletions(-) diff --git a/src/custom-sort/custom-sort-getComparator.spec.ts b/src/custom-sort/custom-sort-getComparator.spec.ts index 1afb404..d942af3 100644 --- a/src/custom-sort/custom-sort-getComparator.spec.ts +++ b/src/custom-sort/custom-sort-getComparator.spec.ts @@ -1,8 +1,6 @@ import { FolderItemForSorting, getComparator, - getSorterFnFor, - getMdata, OS_byCreatedTime, OS_byModifiedTime, OS_byModifiedTimeReverse, SortingLevelId diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index a6a1d3d..d0ee7c8 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -13,9 +13,20 @@ import { sorterByMetadataField, SorterFn } from './custom-sort'; -import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; -import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; -import {findStarredFile_pathParam, Starred_PluginInstance} from "../utils/StarredPluginSignature"; +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec, + RegExpSpec +} from './custom-sort-types'; +import { + CompoundDashNumberNormalizerFn, + CompoundDotRomanNumberNormalizerFn +} from "./sorting-spec-processor"; +import { + findStarredFile_pathParam, + Starred_PluginInstance +} from "../utils/StarredPluginSignature"; import { ObsidianIconFolder_PluginInstance, ObsidianIconFolderPlugin_Data diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index a69ccdd..1cabf1f 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -1,4 +1,6 @@ -import {FolderWildcardMatching} from './folder-matching-rules' +import { + FolderWildcardMatching +} from './folder-matching-rules' type SortingSpec = string diff --git a/src/custom-sort/macros.spec.ts b/src/custom-sort/macros.spec.ts index 487fadf..3011360 100644 --- a/src/custom-sort/macros.spec.ts +++ b/src/custom-sort/macros.spec.ts @@ -1,6 +1,12 @@ -import {expandMacros, expandMacrosInString} from "./macros"; +import { + expandMacros, + expandMacrosInString +} from "./macros"; import * as MacrosModule from './macros' -import {CustomSortGroup, CustomSortSpec} from "./custom-sort-types"; +import { + CustomSortGroup, + CustomSortSpec +} from "./custom-sort-types"; describe('expandMacrosInString', () => { it.each([ diff --git a/src/custom-sort/matchers.spec.ts b/src/custom-sort/matchers.spec.ts index 823c0f8..aef520e 100644 --- a/src/custom-sort/matchers.spec.ts +++ b/src/custom-sort/matchers.spec.ts @@ -12,7 +12,6 @@ import { WordInASCIIRegexStr, WordInAnyLanguageRegexStr } from "./matchers"; -import {SortingSpecProcessor} from "./sorting-spec-processor"; describe('Plain numbers regexp', () => { let regexp: RegExp; diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 63ed0de..8656ccd 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -14,8 +14,16 @@ import { RomanNumberNormalizerFn, SortingSpecProcessor } from "./sorting-spec-processor" -import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; -import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec, + IdentityNormalizerFn +} from "./custom-sort-types"; +import { + FolderMatchingRegexp, + FolderMatchingTreeNode +} from "./folder-matching-rules"; const txtInputExampleA: string = ` order-asc: a-z diff --git a/src/main.ts b/src/main.ts index ce2e52e..d0c8f69 100644 --- a/src/main.ts +++ b/src/main.ts @@ -118,8 +118,6 @@ export default class CustomSortPlugin extends Plugin { this.sortSpecCache, true // Implicit sorting spec generation ) - console.log('Auto injected sort spec') - console.log(this.sortSpecCache) } Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => { @@ -233,6 +231,12 @@ export default class CustomSortPlugin extends Plugin { } } + // Syntax sugar + const ForceFlushCache = true + if (!this.settings.suspended) { + getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, ForceFlushCache) + } + if (fileExplorerView) { if (this.fileExplorerFolderPatched) { fileExplorerView.requestSort(); @@ -338,7 +342,6 @@ export default class CustomSortPlugin extends Plugin { item.setIcon('hashtag'); item.onClick(() => { const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference) - console.log(`custom-sort: bookmark this clicked ${source} and the leaf is`) if (bookmarksPlugin) { bookmarksPlugin.bookmarkFolderItem(file) bookmarksPlugin.saveDataAndUpdateBookmarkViews(true) @@ -349,7 +352,6 @@ export default class CustomSortPlugin extends Plugin { item.setTitle('Custom sort: bookmark+siblings for sorting.'); item.setIcon('hashtag'); item.onClick(() => { - console.log(`custom-sort: bookmark all siblings clicked ${source}`) const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference) if (bookmarksPlugin) { const orderedChildren: Array = plugin.orderedFolderItemsForBookmarking(file.parent) @@ -612,11 +614,11 @@ class CustomSortSettingTab extends PluginSettingTab { 'If enabled, order of files and folders in File Explorer will reflect the order ' + 'of bookmarked items in the bookmarks (core plugin) view. Automatically, without any ' + 'need for sorting configuration. At the same time, it integrates seamlessly with' - + '
sorting-spec:
configurations and they can nicely cooperate.' + + '
sorting-spec:
configurations and they can nicely cooperate.' + '
' + '

To separate regular bookmarks from the bookmarks created for sorting, you can put ' + 'the latter in a separate dedicated bookmarks group. The default name of the group is ' - + "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' " + + "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' " + 'and you can change the group name in the configuration field below.' + '
' + 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.

' @@ -628,7 +630,6 @@ class CustomSortSettingTab extends PluginSettingTab { new Setting(containerEl) .setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)') - // TODO: add a nice description here .setDesc(bookmarksIntegrationDescription) .addToggle(toggle => toggle .setValue(this.plugin.settings.automaticBookmarksIntegration) @@ -661,30 +662,5 @@ class CustomSortSettingTab extends PluginSettingTab { this.plugin.settings.bookmarksContextMenus = value; await this.plugin.saveSettings(); })) - .addButton(cb => cb - .setButtonText('Bt1') - ) - .addExtraButton(cb => cb - .setIcon('clock') - ) - - } } - -// TODO: clear bookmarks cache upon each tap on ribbon or on the command of 'sorting-on' - -// TODO: clear bookmarks cache upon each context menu - before and after (maybe after is not needed, implicitly empty after first clearing) - -// TODO: in discussion sections add (and pin) announcement "DRAG & DROP ORDERING AVAILABLE VIA THE BOOKMARKS CORE PLUGIN INTEGRATION" - -// TODO: in community, add update message with announcement of drag & drop support via Bookmarks plugin - -// TODO: context menu only if bookmarks plugin enabled and new setting (yet to be exposed) doesn't disable it - -// TODO: defensive programming with ?. and equivalents to protect against crash if Obsidian API changes -// Better the plugin to fail an operation than crash with errors - -// TODO: remove console.log (many places added) - -// TODO: ctx menu 'show in bookmarks' instead of 'bookmrk this' diff --git a/src/utils/BookmarksCorePluginSignature.ts b/src/utils/BookmarksCorePluginSignature.ts index cc6d444..1c76740 100644 --- a/src/utils/BookmarksCorePluginSignature.ts +++ b/src/utils/BookmarksCorePluginSignature.ts @@ -1,5 +1,14 @@ -import {InstalledPlugin, PluginInstance, TAbstractFile, TFile, TFolder} from "obsidian"; -import {extractParentFolderPath, lastPathComponent} from "./utils"; +import { + InstalledPlugin, + PluginInstance, + TAbstractFile, + TFile, + TFolder +} from "obsidian"; +import { + extractParentFolderPath, + lastPathComponent +} from "./utils"; const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks' @@ -46,7 +55,7 @@ export interface OrderedBookmarkedItem { group: boolean path: BookmarkedItemPath order: number - pathOfBookmarkGroupsMatches: boolean + bookmarkPathOverlap: number|true // how much the location in bookmarks hierarchy matches the actual file/folder path } interface OrderedBookmarks { @@ -108,7 +117,6 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface { } bookmarkSiblings = (siblings: Array, inTheTop?: boolean) => { - console.log('In this.bookmarksSiblings()') if (siblings.length === 0) return // for sanity const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks( @@ -162,12 +170,9 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface { export const BookmarksCorePluginId: string = 'bookmarks' -export const getBookmarksPlugin = (bookmarksGroupName?: string): BookmarksPluginInterface | undefined => { - invalidateExpiredBookmarksCache() +export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean): BookmarksPluginInterface | undefined => { + invalidateExpiredBookmarksCache(forceFlushCache) const installedBookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId) - console.log(installedBookmarksPlugin) - const bookmarks = (installedBookmarksPlugin?.instance as any) ?.['getBookmarks']() - console.log(bookmarks) if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) { const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance // defensive programming, in case Obsidian changes its internal APIs @@ -204,11 +209,11 @@ const invalidateExpiredBookmarksCache = (force?: boolean): void => { type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void -const traverseBookmarksCollection = (items: Array, callback: TraverseCallback) => { +const traverseBookmarksCollection = (items: Array, callbackConsumeItem: TraverseCallback) => { const recursiveTraversal = (collection: Array, groupsPath: string) => { for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) { const item = collectionRef[idx]; - if (callback(item, groupsPath)) return; + if (callbackConsumeItem(item, groupsPath)) return; if ('group' === item.type) recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${item.title}`); } }; @@ -217,16 +222,21 @@ const traverseBookmarksCollection = (items: Array, callback: Tra const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-' +const bookmarkLocationAndPathOverlap = (bookmarkParentGroupPath: string, fileOrFolderPath: string): number => { + return fileOrFolderPath?.startsWith(bookmarkParentGroupPath) ? bookmarkParentGroupPath.length : 0 +} + 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]() - if (bookmarks) { + let bookmarksItems: Array | undefined = plugin?.[BookmarksPlugin_items_collectionName] + if (bookmarksItems) { if (bookmarksGroupName) { - const bookmarksGroup: BookmarkedGroup|undefined = bookmarks.find( - (item) => item.type === 'group' && item.title === bookmarksGroupName) as BookmarkedGroup - bookmarks = bookmarksGroup ? bookmarksGroup.items : undefined + // scanning only top level items because by desing the bookmarks group for sorting is a top level item + const bookmarksGroup: BookmarkedGroup|undefined = bookmarksItems.find( + (item) => item.type === 'group' && item.title === bookmarksGroupName + ) as BookmarkedGroup + bookmarksItems = bookmarksGroup ? bookmarksGroup.items : undefined } - if (bookmarks) { + if (bookmarksItems) { const orderedBookmarks: OrderedBookmarks = {} let order: number = 0 const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => { @@ -237,24 +247,36 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupNam 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 - // TODO: consume the occurrence at correct folders (groups) location resembling the original structure with highest prio - // and only if not found, consider any (first) occurrence const alreadyConsumed = orderedBookmarks[path] - const pathOfBookmarkGroupsMatches: boolean = true // TODO: !!! with fresh head determine the condition to check here - if (!alreadyConsumed || (pathOfBookmarkGroupsMatches && !alreadyConsumed.pathOfBookmarkGroupsMatches)) { - orderedBookmarks[path] = { - path: path, - order: order++, - file: isFile, - folder: isFile, - group: isGroup, - pathOfBookmarkGroupsMatches: pathOfBookmarkGroupsMatches + + // for groups (they represent folders from sorting perspective) bookmark them unconditionally + // the idea of better match is not applicable + if (alreadyConsumed && isGroup && alreadyConsumed.group) { + return + } + + // for files and folders (folder can be only manually bookmarked, the plugin uses groups to represent folders) + // the most closely matching location in bookmarks hierarchy is preferred + let pathOverlapLength: number|undefined + if (alreadyConsumed && (isFile || isFolder)) { + pathOverlapLength = bookmarkLocationAndPathOverlap(parentGroupsPath, path) + if (pathOverlapLength <= alreadyConsumed.bookmarkPathOverlap) { + return } } + + orderedBookmarks[path] = { + path: path, + order: order++, + file: isFile, + folder: isFile, + group: isGroup, + bookmarkPathOverlap: isGroup || (pathOverlapLength ?? bookmarkLocationAndPathOverlap(parentGroupsPath, path)) + } } } - traverseBookmarksCollection(bookmarks, consumeItem) + + traverseBookmarksCollection(bookmarksItems, consumeItem) return orderedBookmarks } } @@ -318,13 +340,10 @@ const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: bool const CreateIfMissing = true const DontCreateIfMissing = false - - const updateSortingBookmarksAfterItemRenamed = (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 @@ -365,8 +384,6 @@ const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance // 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}`) } const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance, deletedItem: TAbstractFile, bookmarksGroup?: string) => { @@ -390,6 +407,4 @@ const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance if (!item) return; originalContainer.items.remove(item) - - console.log(`Folder deletel ${deletedItem.path}`) }