#74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement

- completed!
- need to see if additional unit tests can be created
This commit is contained in:
SebastianMC 2023-09-28 16:08:24 +02:00
parent bb2b510ae8
commit a07f55a037
8 changed files with 96 additions and 81 deletions

View File

@ -1,8 +1,6 @@
import { import {
FolderItemForSorting, FolderItemForSorting,
getComparator, getComparator,
getSorterFnFor,
getMdata,
OS_byCreatedTime, OS_byCreatedTime,
OS_byModifiedTime, OS_byModifiedTime,
OS_byModifiedTimeReverse, SortingLevelId OS_byModifiedTimeReverse, SortingLevelId

View File

@ -13,9 +13,20 @@ import {
sorterByMetadataField, sorterByMetadataField,
SorterFn SorterFn
} from './custom-sort'; } from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; CustomSortGroupType,
import {findStarredFile_pathParam, Starred_PluginInstance} from "../utils/StarredPluginSignature"; CustomSortOrder,
CustomSortSpec,
RegExpSpec
} from './custom-sort-types';
import {
CompoundDashNumberNormalizerFn,
CompoundDotRomanNumberNormalizerFn
} from "./sorting-spec-processor";
import {
findStarredFile_pathParam,
Starred_PluginInstance
} from "../utils/StarredPluginSignature";
import { import {
ObsidianIconFolder_PluginInstance, ObsidianIconFolder_PluginInstance,
ObsidianIconFolderPlugin_Data ObsidianIconFolderPlugin_Data

View File

@ -1,4 +1,6 @@
import {FolderWildcardMatching} from './folder-matching-rules' import {
FolderWildcardMatching
} from './folder-matching-rules'
type SortingSpec = string type SortingSpec = string

View File

@ -1,6 +1,12 @@
import {expandMacros, expandMacrosInString} from "./macros"; import {
expandMacros,
expandMacrosInString
} from "./macros";
import * as MacrosModule from './macros' import * as MacrosModule from './macros'
import {CustomSortGroup, CustomSortSpec} from "./custom-sort-types"; import {
CustomSortGroup,
CustomSortSpec
} from "./custom-sort-types";
describe('expandMacrosInString', () => { describe('expandMacrosInString', () => {
it.each([ it.each([

View File

@ -12,7 +12,6 @@ import {
WordInASCIIRegexStr, WordInASCIIRegexStr,
WordInAnyLanguageRegexStr WordInAnyLanguageRegexStr
} from "./matchers"; } from "./matchers";
import {SortingSpecProcessor} from "./sorting-spec-processor";
describe('Plain numbers regexp', () => { describe('Plain numbers regexp', () => {
let regexp: RegExp; let regexp: RegExp;

View File

@ -14,8 +14,16 @@ import {
RomanNumberNormalizerFn, RomanNumberNormalizerFn,
SortingSpecProcessor SortingSpecProcessor
} from "./sorting-spec-processor" } from "./sorting-spec-processor"
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; import {
import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; CustomSortGroupType,
CustomSortOrder,
CustomSortSpec,
IdentityNormalizerFn
} from "./custom-sort-types";
import {
FolderMatchingRegexp,
FolderMatchingTreeNode
} from "./folder-matching-rules";
const txtInputExampleA: string = ` const txtInputExampleA: string = `
order-asc: a-z order-asc: a-z

View File

@ -118,8 +118,6 @@ export default class CustomSortPlugin extends Plugin {
this.sortSpecCache, this.sortSpecCache,
true // Implicit sorting spec generation true // Implicit sorting spec generation
) )
console.log('Auto injected sort spec')
console.log(this.sortSpecCache)
} }
Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => { 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 (fileExplorerView) {
if (this.fileExplorerFolderPatched) { if (this.fileExplorerFolderPatched) {
fileExplorerView.requestSort(); fileExplorerView.requestSort();
@ -338,7 +342,6 @@ export default class CustomSortPlugin extends Plugin {
item.setIcon('hashtag'); item.setIcon('hashtag');
item.onClick(() => { item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference) const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
console.log(`custom-sort: bookmark this clicked ${source} and the leaf is`)
if (bookmarksPlugin) { if (bookmarksPlugin) {
bookmarksPlugin.bookmarkFolderItem(file) bookmarksPlugin.bookmarkFolderItem(file)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true) bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
@ -349,7 +352,6 @@ export default class CustomSortPlugin extends Plugin {
item.setTitle('Custom sort: bookmark+siblings for sorting.'); item.setTitle('Custom sort: bookmark+siblings for sorting.');
item.setIcon('hashtag'); item.setIcon('hashtag');
item.onClick(() => { item.onClick(() => {
console.log(`custom-sort: bookmark all siblings clicked ${source}`)
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference) const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) { if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent) const orderedChildren: Array<TAbstractFile> = 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 ' '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 ' + 'of bookmarked items in the bookmarks (core plugin) view. Automatically, without any '
+ 'need for sorting configuration. At the same time, it integrates seamlessly with' + 'need for sorting configuration. At the same time, it integrates seamlessly with'
+ '<pre style="display: inline;">sorting-spec:</pre> configurations and they can nicely cooperate.' + ' <pre style="display: inline;">sorting-spec:</pre> configurations and they can nicely cooperate.'
+ '<br>' + '<br>'
+ '<p>To separate regular bookmarks from the bookmarks created for sorting, you can put ' + '<p>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 ' + 'the latter in a separate dedicated bookmarks group. The default name of the group is '
+ "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' " + "'<i>" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "</i>' "
+ 'and you can change the group name in the configuration field below.' + 'and you can change the group name in the configuration field below.'
+ '<br>' + '<br>'
+ 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.</p>' + 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.</p>'
@ -628,7 +630,6 @@ class CustomSortSettingTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)') .setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)')
// TODO: add a nice description here
.setDesc(bookmarksIntegrationDescription) .setDesc(bookmarksIntegrationDescription)
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.automaticBookmarksIntegration) .setValue(this.plugin.settings.automaticBookmarksIntegration)
@ -661,30 +662,5 @@ class CustomSortSettingTab extends PluginSettingTab {
this.plugin.settings.bookmarksContextMenus = value; this.plugin.settings.bookmarksContextMenus = value;
await this.plugin.saveSettings(); 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'

View File

@ -1,5 +1,14 @@
import {InstalledPlugin, PluginInstance, TAbstractFile, TFile, TFolder} from "obsidian"; import {
import {extractParentFolderPath, lastPathComponent} from "./utils"; InstalledPlugin,
PluginInstance,
TAbstractFile,
TFile,
TFolder
} from "obsidian";
import {
extractParentFolderPath,
lastPathComponent
} from "./utils";
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks' const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
@ -46,7 +55,7 @@ export interface OrderedBookmarkedItem {
group: boolean group: boolean
path: BookmarkedItemPath path: BookmarkedItemPath
order: number order: number
pathOfBookmarkGroupsMatches: boolean bookmarkPathOverlap: number|true // how much the location in bookmarks hierarchy matches the actual file/folder path
} }
interface OrderedBookmarks { interface OrderedBookmarks {
@ -108,7 +117,6 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
} }
bookmarkSiblings = (siblings: Array<TAbstractFile>, inTheTop?: boolean) => { bookmarkSiblings = (siblings: Array<TAbstractFile>, inTheTop?: boolean) => {
console.log('In this.bookmarksSiblings()')
if (siblings.length === 0) return // for sanity if (siblings.length === 0) return // for sanity
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks( const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
@ -162,12 +170,9 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
export const BookmarksCorePluginId: string = 'bookmarks' export const BookmarksCorePluginId: string = 'bookmarks'
export const getBookmarksPlugin = (bookmarksGroupName?: string): BookmarksPluginInterface | undefined => { export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean): BookmarksPluginInterface | undefined => {
invalidateExpiredBookmarksCache() invalidateExpiredBookmarksCache(forceFlushCache)
const installedBookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId) 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) { if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) {
const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance
// defensive programming, in case Obsidian changes its internal APIs // 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 type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => { const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsumeItem: TraverseCallback) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => { const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) { for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
const item = collectionRef[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}`); if ('group' === item.type) recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${item.title}`);
} }
}; };
@ -217,16 +222,21 @@ const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: Tra
const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-' 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 => { const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): OrderedBookmarks | undefined => {
console.log(`Populating bookmarks cache with group scope ${bookmarksGroupName}`) let bookmarksItems: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_items_collectionName]
let bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]() if (bookmarksItems) {
if (bookmarks) {
if (bookmarksGroupName) { if (bookmarksGroupName) {
const bookmarksGroup: BookmarkedGroup|undefined = bookmarks.find( // scanning only top level items because by desing the bookmarks group for sorting is a top level item
(item) => item.type === 'group' && item.title === bookmarksGroupName) as BookmarkedGroup const bookmarksGroup: BookmarkedGroup|undefined = bookmarksItems.find(
bookmarks = bookmarksGroup ? bookmarksGroup.items : undefined (item) => item.type === 'group' && item.title === bookmarksGroupName
) as BookmarkedGroup
bookmarksItems = bookmarksGroup ? bookmarksGroup.items : undefined
} }
if (bookmarks) { if (bookmarksItems) {
const orderedBookmarks: OrderedBookmarks = {} const orderedBookmarks: OrderedBookmarks = {}
let order: number = 0 let order: number = 0
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => { const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
@ -237,24 +247,36 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupNam
if ((isFile && hasSortspecAnchor) || isFolder || isGroup) { if ((isFile && hasSortspecAnchor) || isFolder || isGroup) {
const pathOfGroup: string = `${parentGroupsPath}${parentGroupsPath?'/':''}${item.title}` const pathOfGroup: string = `${parentGroupsPath}${parentGroupsPath?'/':''}${item.title}`
const path = isGroup ? pathOfGroup : (item as BookmarkWithPath).path 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 alreadyConsumed = orderedBookmarks[path]
const pathOfBookmarkGroupsMatches: boolean = true // TODO: !!! with fresh head determine the condition to check here
if (!alreadyConsumed || (pathOfBookmarkGroupsMatches && !alreadyConsumed.pathOfBookmarkGroupsMatches)) { // for groups (they represent folders from sorting perspective) bookmark them unconditionally
orderedBookmarks[path] = { // the idea of better match is not applicable
path: path, if (alreadyConsumed && isGroup && alreadyConsumed.group) {
order: order++, return
file: isFile, }
folder: isFile,
group: isGroup, // for files and folders (folder can be only manually bookmarked, the plugin uses groups to represent folders)
pathOfBookmarkGroupsMatches: pathOfBookmarkGroupsMatches // 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 return orderedBookmarks
} }
} }
@ -318,13 +340,10 @@ const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: bool
const CreateIfMissing = true const CreateIfMissing = true
const DontCreateIfMissing = false const DontCreateIfMissing = false
const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => { const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => {
if (renamedItem.path === oldPath) return; // sanity if (renamedItem.path === oldPath) return; // sanity
let items = plugin[BookmarksPlugin_items_collectionName]
const aFolder: boolean = renamedItem instanceof TFolder const aFolder: boolean = renamedItem instanceof TFolder
const aFolderWithChildren: boolean = aFolder && (renamedItem as TFolder).children.length > 0 const aFolderWithChildren: boolean = aFolder && (renamedItem as TFolder).children.length > 0
const aFile: boolean = !aFolder 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 // because folders are represented (for sorting purposes) by groups with exact name
(item as BookmarkedGroup).title = newName (item as BookmarkedGroup).title = newName
} }
console.log(`Folder renamel from ${oldPath} to ${renamedItem.path}`)
} }
const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance, deletedItem: TAbstractFile, bookmarksGroup?: string) => { const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance, deletedItem: TAbstractFile, bookmarksGroup?: string) => {
@ -390,6 +407,4 @@ const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance
if (!item) return; if (!item) return;
originalContainer.items.remove(item) originalContainer.items.remove(item)
console.log(`Folder deletel ${deletedItem.path}`)
} }