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

- feature code complete
- not reviewed
- not tested
- no unit tests coverage
This commit is contained in:
SebastianMC 2023-04-07 18:40:35 +02:00
parent 08ffd7db9a
commit 56348006ce
7 changed files with 407 additions and 29 deletions

View File

@ -9,6 +9,7 @@ export enum CustomSortGroupType {
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
StarredOnly,
BookmarkedOnly,
HasIcon
}
@ -30,6 +31,8 @@ export enum CustomSortOrder {
byMetadataFieldAlphabeticalReverse,
byMetadataFieldTrueAlphabeticalReverse,
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
byBookmarkOrder,
byBookmarkOrderReverse,
default = alphabetical
}

View File

@ -5,7 +5,7 @@ import {
determineFolderDatesIfNeeded,
determineSortingGroup,
FolderItemForSorting,
matchGroupRegex,
matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField,
SorterFn,
Sorters
} from './custom-sort';
@ -16,6 +16,7 @@ import {
ObsidianIconFolder_PluginInstance,
ObsidianIconFolderPlugin_Data
} from "../utils/ObsidianIconFolderPluginSignature";
import {determineBookmarkOrder} from "../utils/BookmarksCorePluginSignature";
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
return {
@ -2216,7 +2217,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2226,6 +2227,25 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
expect(result2).toBe(SORT_FIRST_GOES_LATER)
})
it('should put the item with metadata later if the second one has no metadata (reverse order)', () => {
// given
const itemA: Partial<FolderItemForSorting> = {
metadataFieldValue: '15',
sortString: 'n123'
}
const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
// then
expect(result1).toBe(SORT_FIRST_GOES_LATER)
expect(result2).toBe(SORT_FIRST_GOES_EARLIER)
})
it('should correctly fallback to alphabetical reverse if no metadata on both items', () => {
// given
const itemA: Partial<FolderItemForSorting> = {
@ -2247,3 +2267,74 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
})
})
describe('sorterByMetadataField', () => {
it.each([
[true,'abc','def',-1, 'a', 'a'],
[true,'xyz','klm',1, 'b', 'b'],
[true,'mmm','mmm',0, 'c', 'c'],
[true,'mmm','mmm',-1, 'd', 'e'],
[true,'mmm','mmm',1, 'e', 'd'],
[true,'abc',undefined,-1, 'a','a'],
[true,undefined,'klm',1, 'b','b'],
[true,undefined,undefined,0, 'a','a'],
[true,undefined,undefined,-1, 'a','b'],
[true,undefined,undefined,1, 'd','c'],
[false,'abc','def',1, 'a', 'a'],
[false,'xyz','klm',-1, 'b', 'b'],
[false,'mmm','mmm',0, 'c', 'c'],
[false,'mmm','mmm',1, 'd', 'e'],
[false,'mmm','mmm',-1, 'e', 'd'],
[false,'abc',undefined,1, 'a','a'],
[false,undefined,'klm',-1, 'b','b'],
[false,undefined,undefined,0, 'a','a'],
[false,undefined,undefined,1, 'a','b'],
[false,undefined,undefined,-1, 'd','c'],
])('straight order %s, comparing %s and %s should return %s for sortStrings %s and %s',
(straight: boolean, metadataA: string|undefined, metadataB: string|undefined, order: number, sortStringA: string, sortStringB) => {
const sorterFn = sorterByMetadataField(!straight, false)
const itemA: Partial<FolderItemForSorting> = {metadataFieldValue: metadataA, sortString: sortStringA}
const itemB: Partial<FolderItemForSorting> = {metadataFieldValue: metadataB, sortString: sortStringB}
const result = sorterFn(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
// then
expect(result).toBe(order)
})
})
describe('sorterByBookmarkOrder', () => {
it.each([
[true,10,20,-1, 'a', 'a'],
[true,20,10,1, 'b', 'b'],
[true,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
[true,1,1,0, 'd', 'e'], // ----//----
[true,2,2,0, 'e', 'd'], // ----//----
[true,3,undefined,-1, 'a','a'],
[true,undefined,4,1, 'b','b'],
[true,undefined,undefined,0, 'a','a'],
[true,undefined,undefined,-1, 'a','b'],
[true,undefined,undefined,1, 'd','c'],
[false,10,20,1, 'a', 'a'],
[false,20,10,-1, 'b', 'b'],
[false,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
[false,1,1,0, 'd', 'e'], // ------//-----
[false,2,2,0, 'e', 'd'], // ------//-----
[false,3,undefined,1, 'a','a'],
[false,undefined,4,-1, 'b','b'],
[false,undefined,undefined,0, 'a','a'],
[false,undefined,undefined,1, 'a','b'],
[false,undefined,undefined,-1, 'd','c'],
])('straight order %s, comparing %s and %s should return %s for sortStrings %s and %s',
(straight: boolean, bookmarkA: number|undefined, bookmarkB: number|undefined, order: number, sortStringA: string, sortStringB) => {
const sorterFn = sorterByBookmarkOrder(!straight, false)
const itemA: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkA, sortString: sortStringA}
const itemB: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkB, sortString: sortStringB}
const result = sorterFn(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
const normalizedResult = result < 0 ? -1 : ((result > 0) ? 1 : result)
// then
expect(normalizedResult).toBe(order)
})
})

View File

@ -1,26 +1,9 @@
import {
App,
CommunityPlugin,
FrontMatterCache,
InstalledPlugin,
requireApiVersion,
TAbstractFile,
TFile,
TFolder
} from 'obsidian';
import {
determineStarredStatusOf,
getStarredPlugin,
Starred_PluginInstance,
StarredPlugin_findStarredFile_methodName
} from '../utils/StarredPluginSignature';
import {FrontMatterCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
import {determineStarredStatusOf, getStarredPlugin, Starred_PluginInstance} from '../utils/StarredPluginSignature';
import {
determineIconOf,
getIconFolderPlugin,
FolderIconObject,
ObsidianIconFolder_PluginInstance,
ObsidianIconFolderPlugin_Data,
ObsidianIconFolderPlugin_getData_methodName
ObsidianIconFolder_PluginInstance
} from '../utils/ObsidianIconFolderPluginSignature'
import {
CustomSortGroup,
@ -32,6 +15,11 @@ import {
RegExpSpec
} from "./custom-sort-types";
import {isDefined} from "../utils/utils";
import {
Bookmarks_PluginInstance,
determineBookmarkOrder,
getBookmarksPlugin
} from "../utils/BookmarksCorePluginSignature";
let CollatorCompare = new Intl.Collator(undefined, {
usage: "sort",
@ -45,6 +33,21 @@ let CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, {
numeric: false,
}).compare;
export const SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION: CustomSortSpec = {
defaultOrder: CustomSortOrder.byBookmarkOrder,
groups: [
{
order: CustomSortOrder.byBookmarkOrder,
type: CustomSortGroupType.Outsiders
}
],
outsidersGroupIdx: 0,
targetFoldersPaths: [
"Spec applied automatically to folder not having explicit spec when automatic integration with bookmarks is enabled"
]
}
export interface FolderItemForSorting {
path: string
groupIdx?: number // the index itself represents order for groups
@ -55,6 +58,7 @@ export interface FolderItemForSorting {
mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file
isFolder: boolean
folder?: TFolder
bookmarkedIdx?: number // derived from Bookmarks core plugin position
}
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
@ -65,7 +69,7 @@ const TrueAlphabetical: boolean = true
const ReverseOrder: boolean = true
const StraightOrder: boolean = false
const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
export const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
if (reverseOrder) {
@ -81,13 +85,31 @@ const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean)
}
}
// Item with metadata goes before the w/o metadata
if (a.metadataFieldValue) return reverseOrder ? 1 : -1
if (b.metadataFieldValue) return reverseOrder ? -1 : 1
if (a.metadataFieldValue) return -1
if (b.metadataFieldValue) return 1
// Fallback -> requested sort by metadata, yet none of two items contain it, use alphabetical by name
return collatorCompareFn(a.sortString, b.sortString)
}
}
export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
if (reverseOrder) {
[a, b] = [b, a]
}
if (a.bookmarkedIdx && b.bookmarkedIdx) {
// By design the bookmark idx is unique per each item, so no need for secondary sorting if they are equal
return a.bookmarkedIdx - b.bookmarkedIdx
}
// Item with bookmark order goes before the w/o bookmark info
if (a.bookmarkedIdx) return -1
if (b.bookmarkedIdx) return 1
// Fallback -> requested sort by bookmark order, yet none of two items contain it, use alphabetical by name
return collatorCompareFn(a.sortString, b.sortString)
}
}
export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
@ -105,6 +127,8 @@ export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical),
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder),
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical),
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
@ -164,6 +188,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
export interface Context {
starredPluginInstance?: Starred_PluginInstance
bookmarksPluginInstance?: Bookmarks_PluginInstance
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
}
@ -171,6 +196,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
let groupIdx: number
let determined: boolean = false
let matchedGroup: string | null | undefined
let bookmarkedIdx: number | undefined
let metadataValueToSortBy: string | undefined
const aFolder: boolean = isFolder(entry)
const aFile: boolean = !aFolder
@ -264,12 +290,20 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
break
case CustomSortGroupType.StarredOnly:
if (ctx?.starredPluginInstance) {
let starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance)
const starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance)
if (starred) {
determined = true
}
}
break
case CustomSortGroupType.BookmarkedOnly:
if (ctx?.bookmarksPluginInstance) {
const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPluginInstance)
if (bookmarkOrder) { // safe ==> orders intentionally start from 1
determined = true
bookmarkedIdx = bookmarkOrder
}
}
case CustomSortGroupType.HasIcon:
if(ctx?.iconFolderPluginInstance) {
let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance)
@ -363,7 +397,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
folder: aFolder ? (entry as TFolder) : undefined,
path: entry.path,
ctime: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME,
bookmarkedIdx: bookmarkedIdx
}
}
@ -380,6 +415,17 @@ export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, se
|| SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian)
}
const SortOrderRequiringBookmarksOrder = new Set<CustomSortOrder>([
CustomSortOrder.byBookmarkOrder,
CustomSortOrder.byBookmarkOrderReverse
])
export const sortOrderNeedsBookmarksOrder = (order: CustomSortOrder | undefined, secondary?: CustomSortOrder): boolean => {
// The CustomSortOrder.standardObsidian used as default because it doesn't require bookmarks order
return SortOrderRequiringBookmarksOrder.has(order ?? CustomSortOrder.standardObsidian)
|| SortOrderRequiringBookmarksOrder.has(secondary ?? CustomSortOrder.standardObsidian)
}
// Syntax sugar for readability
export type ModifiedTime = number
export type CreatedTime = number
@ -422,10 +468,32 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
})
}
// Order by bookmarks order can be applied independently of grouping by bookmarked status
// This function determines the bookmarked order if the sorting criteria (of group or entire folder) requires it
export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemForSorting>, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance) => {
if (!plugin) return
folderItems.forEach((item) => {
const folderDefaultSortRequiresBookmarksOrder: boolean = !!(sortingSpec.defaultOrder && sortOrderNeedsBookmarksOrder(sortingSpec.defaultOrder))
let groupSortRequiresBookmarksOrder: boolean = false
if (!folderDefaultSortRequiresBookmarksOrder) {
const groupIdx: number | undefined = item.groupIdx
if (groupIdx !== undefined) {
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
groupSortRequiresBookmarksOrder = sortOrderNeedsBookmarksOrder(groupOrder)
}
}
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) {
item.bookmarkedIdx = determineBookmarkOrder(item.path, plugin)
}
})
}
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app)
const bookmarksPluginInstance: Bookmarks_PluginInstance | undefined = getBookmarksPlugin(sortingSpec?.plugin?.app)
const iconFolderPluginInstance: ObsidianIconFolder_PluginInstance | undefined = getIconFolderPlugin(sortingSpec?.plugin?.app)
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
@ -437,6 +505,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
.map((entry: TFile | TFolder) => {
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, {
starredPluginInstance: starredPluginInstance,
bookmarksPluginInstance: bookmarksPluginInstance,
iconFolderPluginInstance: iconFolderPluginInstance
})
return itemForSorting
@ -445,6 +514,10 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
determineFolderDatesIfNeeded(folderItems, sortingSpec)
if (bookmarksPluginInstance) {
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, bookmarksPluginInstance)
}
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
return compareTwoItems(itA, itB, sortingSpec);
});

View File

@ -37,6 +37,13 @@ starred:
/:files starred:
/folders starred:
:::: folder of bookmarks
< by-bookmarks-order
/: bookmarked:
< by-bookmarks-order
/ Abc
> by-bookmarks-order
:::: Conceptual model
/: Entities
%
@ -95,6 +102,13 @@ target-folder: tricky folder 2
/:files starred:
/folders starred:
target-folder: folder of bookmarks
order-asc: by-bookmarks-order
/:files bookmarked:
order-asc: by-bookmarks-order
/folders Abc
order-desc: by-bookmarks-order
:::: Conceptual model
/:files Entities
%
@ -205,6 +219,30 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
'tricky folder 2'
]
},
"folder of bookmarks": {
defaultOrder: CustomSortOrder.byBookmarkOrder,
groups: [
{
filesOnly: true,
order: CustomSortOrder.byBookmarkOrder,
type: CustomSortGroupType.BookmarkedOnly
},
{
exactText: "Abc",
foldersOnly: true,
order: CustomSortOrder.byBookmarkOrderReverse,
type: CustomSortGroupType.ExactName
},
{
order: CustomSortOrder.byBookmarkOrder,
type: CustomSortGroupType.Outsiders
}
],
outsidersGroupIdx: 2,
targetFoldersPaths: [
"folder of bookmarks"
]
},
"Conceptual model": {
groups: [{
exactText: "Entities",

View File

@ -114,6 +114,7 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
// Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name
'a-z, created': {
@ -207,6 +208,8 @@ const HideItemVerboseLexeme: string = '/--hide:'
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
const StarredItemsIndicatorLexeme: string = 'starred:'
const IconIndicatorLexeme: string = 'with-icon:'
@ -1514,6 +1517,13 @@ export class SortingSpecProcessor {
foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt
}
} else if (theOnly.startsWith(BookmarkedItemIndicatorLexeme)) {
return {
type: CustomSortGroupType.BookmarkedOnly,
filesOnly: spec.filesOnly,
foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt
}
} else if (theOnly.startsWith(IconIndicatorLexeme)) {
const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length))
return {

View File

@ -16,7 +16,7 @@ import {
Vault
} from 'obsidian';
import {around} from 'monkey-around';
import {folderSort} from './custom-sort/custom-sort';
import {folderSort, SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION} from './custom-sort/custom-sort';
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
@ -36,6 +36,7 @@ interface CustomSortPluginSettings {
statusBarEntryEnabled: boolean
notificationsEnabled: boolean
mobileNotificationsEnabled: boolean
enableAutomaticBookmarksOrderIntegration: boolean
}
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
@ -43,7 +44,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = {
suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
statusBarEntryEnabled: true,
notificationsEnabled: true,
mobileNotificationsEnabled: false
mobileNotificationsEnabled: false,
enableAutomaticBookmarksOrderIntegration: false
}
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
@ -347,6 +349,9 @@ export default class CustomSortPlugin extends Plugin {
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}
}
if (!sortSpec && plugin.settings.enableAutomaticBookmarksOrderIntegration) {
sortSpec = SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION
}
if (sortSpec) {
sortSpec.plugin = plugin
return folderSort.call(this, sortSpec, ...args);
@ -476,5 +481,18 @@ class CustomSortSettingTab extends PluginSettingTab {
this.plugin.settings.mobileNotificationsEnabled = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable automatic integration with core Bookmarks plugin')
// TODO: add a nice description here
.setDesc('Details TBD. TODO: add a nice description here')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAutomaticBookmarksOrderIntegration)
.onChange(async (value) => {
this.plugin.settings.enableAutomaticBookmarksOrderIntegration = value;
await this.plugin.saveSettings();
}));
// TODO: expose additional configuration setting to specify group path in Bookmarks, if auto-integration with bookmarks is enabled
}
}

View File

@ -0,0 +1,145 @@
import {App, InstalledPlugin, PluginInstance} from "obsidian";
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
type Path = string
// Only relevant types of bookmarked items considered here
// The full set of types also includes 'search', canvas, graph, maybe more to come
type BookmarkedItem = BookmarkedFile | BookmarkedFolder | BookmarkedGroup
// Either a file, a folder or header/block inside a file
interface BookmarkWithPath {
path: Path
}
interface BookmarkedFile {
type: 'file'
path: Path
subpath?: string // Anchor within the file
title?: string
}
interface BookmarkedFolder {
type: 'folder'
path: Path
title?: string
}
interface BookmarkedGroup {
type: 'group'
items: Array<BookmarkedItem>
title?: string
}
export type BookmarkedItemPath = string
export interface OrderedBookmarkedItem {
file: boolean
folder: boolean
path: BookmarkedItemPath
order: number
}
interface OrderedBookmarks {
[key: BookmarkedItemPath]: OrderedBookmarkedItem
}
export interface Bookmarks_PluginInstance extends PluginInstance {
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
}
let bookmarksCache: OrderedBookmarks | undefined = undefined
let bookmarksCacheTimestamp: number | undefined = undefined
const CacheExpirationMilis = 1000 // One second seems to be reasonable
export const invalidateExpiredBookmarksCache = (force?: boolean): void => {
if (bookmarksCache) {
let flush: boolean = true
if (!force && !!bookmarksCacheTimestamp) {
if (Date.now() - CacheExpirationMilis <= bookmarksCacheTimestamp) {
flush = false
}
}
if (flush) {
bookmarksCache = undefined
bookmarksCacheTimestamp = undefined
}
}
}
export const BookmarksCorePluginId: string = 'bookmarks'
export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefined => {
invalidateExpiredBookmarksCache()
const bookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId)
console.log(bookmarksPlugin)
const bookmarks = (bookmarksPlugin?.instance as any) ?.['getBookmarks']()
console.log(bookmarks)
if (bookmarksPlugin && bookmarksPlugin.enabled && bookmarksPlugin.instance) {
const bookmarksPluginInstance: Bookmarks_PluginInstance = bookmarksPlugin.instance as Bookmarks_PluginInstance
// defensive programming, in case Obsidian changes its internal APIs
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function') {
return bookmarksPluginInstance
}
}
}
type TraverseCallback = (item: BookmarkedItem) => boolean | void
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>) => {
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
const item = collectionRef[idx];
if (callback(item)) return;
if ('group' === item.type) recursiveTraversal(item.items);
}
};
recursiveTraversal(items);
}
// TODO: extend this function to take a scope as parameter: a path to Bookmarks group to start from
// Initially consuming all bookmarks is ok - finally the starting point (group) should be configurable
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks | undefined => {
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
if (bookmarks) {
const orderedBookmarks: OrderedBookmarks = {}
let order: number = 0
const consumeItem = (item: BookmarkedItem) => {
const isFile: boolean = item.type === 'file'
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
const isFolder: boolean = item.type === 'folder'
if ((isFile && !isAnchor) || isFolder) {
const path = (item as BookmarkWithPath).path
// Consume only the first occurrence of a path in bookmarks, even if many duplicates can exist
const alreadyConsumed = orderedBookmarks[path]
if (!alreadyConsumed) {
orderedBookmarks[path] = {
path: path,
order: order++,
file: isFile,
folder: isFile
}
}
}
}
traverseBookmarksCollection(bookmarks, consumeItem)
return orderedBookmarks
}
}
// Result:
// undefined ==> item not found in bookmarks
// > 0 ==> item found in bookmarks at returned position
// Intentionally not returning 0 to allow simple syntax of processing the result
export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance): number | undefined => {
if (!bookmarksCache) {
bookmarksCache = getOrderedBookmarks(plugin)
bookmarksCacheTimestamp = Date.now()
}
const bookmarkedItemPosition: number | undefined = bookmarksCache?.[path]?.order
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
}