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

Tons of updates:
- feature complete
  - context menu for 'bookmark this' and 'bookmark+siblings' for sorting
  - order of bookmarked siblings reflects the current sorting in File Explorer, whatever it is (!!!)
- unit tests probably missing for the additions
- list of TODO: updated in main.ts and other files inline
This commit is contained in:
SebastianMC 2023-04-19 16:59:35 +02:00
parent 71ab76652c
commit 99afdebba8
6 changed files with 256 additions and 91 deletions

View File

@ -16,7 +16,6 @@ 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 {

View File

@ -57,6 +57,8 @@ export interface FolderItemForSorting {
}
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
export type PlainSorterFn = (a: TAbstractFile, b: TAbstractFile) => number
export type PlainFileOnlySorterFn = (a: TFile, b: TFile) => number
export type CollatorCompareFn = (a: string, b: string) => number
// Syntax sugar
@ -129,13 +131,32 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
};
// OS - Obsidian Sort
const OS_alphabetical = 'alphabetical'
const OS_alphabeticalReverse = 'alphabeticalReverse'
const OS_byModifiedTime = 'byModifiedTime'
const OS_byModifiedTimeReverse = 'byModifiedTimeReverse'
const OS_byCreatedTime = 'byCreatedTime'
const OS_byCreatedTimeReverse = 'byCreatedTimeReverse'
export const ObsidianStandardDefaultSortingName = OS_alphabetical
const StandardObsidianToCustomSort: {[key: string]: CustomSortOrder} = {
"alphabetical": CustomSortOrder.alphabetical,
"alphabeticalReverse": CustomSortOrder.alphabeticalReverse,
"byModifiedTime": CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
"byModifiedTimeReverse": CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
"byCreatedTime": CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
"byCreatedTimeReverse": CustomSortOrder.byCreatedTime // In Obsidian labeled as 'Created time (old to new)'
[OS_alphabetical]: CustomSortOrder.alphabetical,
[OS_alphabeticalReverse]: CustomSortOrder.alphabeticalReverse,
[OS_byModifiedTime]: CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
[OS_byModifiedTimeReverse]: CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
[OS_byCreatedTime]: CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
[OS_byCreatedTimeReverse]: CustomSortOrder.byCreatedTime // In Obsidian labeled as 'Created time (old to new)'
}
const StandardObsidianToPlainSortFn: {[key: string]: PlainFileOnlySorterFn} = {
[OS_alphabetical]: (a: TFile, b: TFile) => CollatorCompare(a.basename, b.basename),
[OS_alphabeticalReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_alphabetical](a,b),
[OS_byModifiedTime]: (a: TFile, b: TFile) => b.stat.mtime - a.stat.mtime,
[OS_byModifiedTimeReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_byModifiedTime](a,b),
[OS_byCreatedTime]: (a: TFile, b: TFile) => b.stat.ctime - a.stat.ctime,
[OS_byCreatedTimeReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_byCreatedTime](a,b)
}
// Standard Obsidian comparator keeps folders in the top sorted alphabetically
@ -150,6 +171,20 @@ const StandardObsidianComparator = (order: CustomSortOrder): SorterFn => {
}
}
// Equivalent of StandardObsidianComparator working directly on TAbstractFile items
export const StandardPlainObsidianComparator = (order: string): PlainSorterFn => {
const fileSorterFn = StandardObsidianToPlainSortFn[order] || StandardObsidianToCustomSort[OS_alphabetical]
return (a: TAbstractFile, b: TAbstractFile): number => {
const aIsFolder: boolean = a instanceof TFolder
const bIsFolder: boolean = b instanceof TFolder
return aIsFolder || bIsFolder
?
(aIsFolder && !bIsFolder ? -1 : (bIsFolder && !aIsFolder ? 1 : CollatorCompare(a.name,b.name)))
:
fileSorterFn(a as TFile, b as TFile);
}
}
export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => {
if (sorting === CustomSortOrder.standardObsidian) {
sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical
@ -545,3 +580,36 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
this.children = items;
}
};
// Returns a sorted copy of the input array, intentionally to keep it intact
export const sortFolderItemsForBookmarking = function (items: Array<TAbstractFile>, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array<TAbstractFile> {
if (sortingSpec) {
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
const folderItems: Array<FolderItemForSorting> = items.map((entry: TFile | TFolder) => {
folderItemsByPath[entry.path] = entry
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
return itemForSorting
})
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
determineFolderDatesIfNeeded(folderItems, sortingSpec)
if (ctx.bookmarksPlugin?.instance) {
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPlugin.instance, ctx.bookmarksPlugin.groupNameForSorting)
}
const comparator: SorterFn = getComparator(sortingSpec, uiSortOrder)
folderItems.sort(comparator)
const sortedItems: Array<TAbstractFile> = folderItems.map((entry) => folderItemsByPath[entry.path])
return sortedItems
} else { // No custom sorting or the custom sort disabled - apply standard Obsidian sorting (internally 1:1 recreated implementation)
const folderItems: Array<TAbstractFile> = items.map((entry: TFile | TFolder) => entry)
const plainSorterFn: PlainSorterFn = StandardPlainObsidianComparator(uiSortOrder)
folderItems.sort(plainSorterFn)
return folderItems
}
};

View File

@ -16,9 +16,14 @@ import {
Vault, WorkspaceLeaf
} from 'obsidian';
import {around} from 'monkey-around';
import {folderSort, ProcessingContext} from './custom-sort/custom-sort';
import {
folderSort,
ObsidianStandardDefaultSortingName,
ProcessingContext,
sortFolderItemsForBookmarking
} from './custom-sort/custom-sort';
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
import {CustomSortSpec} from './custom-sort/custom-sort-types';
import {
addIcons,
@ -30,8 +35,14 @@ import {
ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons";
import {getStarredPlugin} from "./utils/StarredPluginSignature";
import {getBookmarksPlugin} from "./utils/BookmarksCorePluginSignature";
import {
getBookmarksPlugin,
bookmarkFolderItem,
saveDataAndUpdateBookmarkViews,
bookmarkSiblings
} from "./utils/BookmarksCorePluginSignature";
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
import {lastPathComponent} from "./utils/utils";
interface CustomSortPluginSettings {
additionalSortspecFile: string
@ -317,14 +328,25 @@ export default class CustomSortPlugin extends Plugin {
item.setTitle('Custom sort: bookmark for sorting.');
item.setIcon('hashtag');
item.onClick(() => {
console.log(`custom-sort: bookmark this clicked ${source}`)
const bookmarksPlugin = getBookmarksPlugin(plugin.app)
console.log(`custom-sort: bookmark this clicked ${source} and the leaf is`)
if (bookmarksPlugin) {
bookmarkFolderItem(file, bookmarksPlugin, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
saveDataAndUpdateBookmarkViews(bookmarksPlugin, plugin.app)
}
});
};
const bookmarkAllMenuItem = (item: MenuItem) => {
item.setTitle('Custom sort: bookmark all siblings for sorting.');
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.app)
if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent)
bookmarkSiblings(orderedChildren, bookmarksPlugin, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
saveDataAndUpdateBookmarkViews(bookmarksPlugin, plugin.app)
}
});
};
@ -358,6 +380,32 @@ export default class CustomSortPlugin extends Plugin {
})
}
determineSortSpecForFolder(folderPath: string, folderName?: string): CustomSortSpec|null|undefined {
folderName = folderName ?? lastPathComponent(folderPath)
let sortSpec: CustomSortSpec | null | undefined = this.sortSpecCache?.sortSpecByPath?.[folderPath]
sortSpec = sortSpec ?? this.sortSpecCache?.sortSpecByName?.[folderName]
if (!sortSpec && this.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = this.sortSpecCache?.sortSpecByWildcard.folderMatch(folderPath, folderName)
}
return sortSpec
}
createProcessingContextForSorting(): ProcessingContext {
const ctx: ProcessingContext = {
_mCache: this.app.metadataCache,
starredPluginInstance: getStarredPlugin(this.app),
bookmarksPlugin: {
instance: this.settings.automaticBookmarksIntegration ? getBookmarksPlugin(this.app) : undefined,
groupNameForSorting: this.settings.bookmarksGroupToConsumeAsOrderingReference
},
iconFolderPluginInstance: getIconFolderPlugin(this.app),
plugin: this
}
return ctx
}
// For the idea of monkey-patching credits go to https://github.com/nothingislost/obsidian-bartender
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
let plugin = this;
@ -371,10 +419,6 @@ export default class CustomSortPlugin extends Plugin {
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) {
return function (...args: any[]) {
console.log(this)
console.log(this.fileExplorer.sortOrder)
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
@ -385,38 +429,12 @@ export default class CustomSortPlugin extends Plugin {
setIcon(plugin.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
}
// if custom sort is not specified, use the UI-selected
const folder: TFolder = this.file
let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path]
sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name]
if (!sortSpec && plugin.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path, folder.name)
/* SM??? if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
explicitlyStandardSort = true
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}*/
}
// TODO: ensure that explicitly configured standard sort excludes the auto-applied on-the-fly
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
if (sortSpec) {
console.log(`Sortspec for folder ${folder.path}`)
console.log(sortSpec)
const ctx: ProcessingContext = {
_mCache: plugin.app.metadataCache,
starredPluginInstance: getStarredPlugin(plugin.app),
bookmarksPlugin: {
instance: plugin.settings.automaticBookmarksIntegration ? getBookmarksPlugin(this.app) : undefined,
groupNameForSorting: plugin.settings.bookmarksGroupToConsumeAsOrderingReference
},
iconFolderPluginInstance: getIconFolderPlugin(this.app),
plugin: plugin
}
return folderSort.call(this, sortSpec, ctx);
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting());
} else {
console.log(`NO Sortspec for folder ${folder.path}`)
return old.call(this, ...args);
}
};
@ -429,6 +447,16 @@ export default class CustomSortPlugin extends Plugin {
}
}
orderedFolderItemsForBookmarking(folder: TFolder): Array<TAbstractFile> {
let sortSpec: CustomSortSpec | null | undefined = undefined
if (!this.settings.suspended) {
sortSpec = this.determineSortSpecForFolder(folder.path, folder.name)
}
let uiSortOrder: string = this.getFileExplorer()?.sortOrder || ObsidianStandardDefaultSortingName
return sortFolderItemsForBookmarking(folder.children, sortSpec, this.createProcessingContextForSorting(), uiSortOrder)
}
// Credits go to https://github.com/nothingislost/obsidian-bartender
getFileExplorer(): FileExplorerView | undefined {
let fileExplorer: FileExplorerView | undefined = this.app.workspace.getLeavesOfType("file-explorer")?.first()
@ -437,7 +465,6 @@ export default class CustomSortPlugin extends Plugin {
}
onunload() {
}
updateStatusBar() {
@ -455,6 +482,10 @@ export default class CustomSortPlugin extends Plugin {
}
}
const pathToFlatString = (path: string): string => {
return path.replace('/','_').replace('\\', '_')
}
class CustomSortSettingTab extends PluginSettingTab {
plugin: CustomSortPlugin;
@ -577,33 +608,23 @@ class CustomSortSettingTab extends PluginSettingTab {
.setPlaceholder('e.g. Group for sorting')
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
.onChange(async (value) => {
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim();
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim() ? pathToFlatString(normalizePath(value)) : '';
await this.plugin.saveSettings();
}));
}
}
// 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, implicitlty empty after first clearing)
// TODO: if a folder doesn't have any bookmarked items, it should remain under control of standard obsidian sorting
// 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: if folder has explicit sorting: standard, don't apply bookmarks
// TODO: context menu only if bookmarks plugin enabled and new setting (yet to be exposed) doesn't disable it
// TODO: fix error
// bookmarks integration - for root folder and for other folders
// (check for the case:
// target-folder: /*
// sorting: standard
// 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: unbookmarked items in partially bookmarked -> can it apply the system sort ???
// TODO: unblock syntax 'sorting: standard' also for groups --> since I have access to currently configured sorting :-)
// TODO: bug? On auto-bookmark integration strange behavior
// TODO: remove console.log (many places added)

View File

@ -53,5 +53,7 @@ declare module 'obsidian' {
createFolderDom(folder: TFolder): FileExplorerFolder;
requestSort(): void;
sortOrder: string
}
}

View File

@ -1,7 +1,10 @@
import {App, InstalledPlugin, PluginInstance} from "obsidian";
import {App, InstalledPlugin, Plugin, PluginInstance, TAbstractFile, TFolder} from "obsidian";
import {lastPathComponent} from "./utils";
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
const BookmarksPlugin_items_collectionName = 'items'
type Path = string
// Only relevant types of bookmarked items considered here
@ -16,20 +19,23 @@ interface BookmarkWithPath {
interface BookmarkedFile {
type: 'file'
path: Path
subpath?: string // Anchor within the file
subpath?: string // Anchor within the file (heading and/or block ref)
title?: string
ctime: number
}
interface BookmarkedFolder {
type: 'folder'
path: Path
title?: string
ctime: number
}
interface BookmarkedGroup {
type: 'group'
items: Array<BookmarkedItem>
title?: string
ctime: number
}
export type BookmarkedItemPath = string
@ -37,6 +43,7 @@ export type BookmarkedItemPath = string
export interface OrderedBookmarkedItem {
file: boolean
folder: boolean
group: boolean
path: BookmarkedItemPath
order: number
}
@ -47,6 +54,8 @@ interface OrderedBookmarks {
export interface Bookmarks_PluginInstance extends PluginInstance {
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
[BookmarksPlugin_items_collectionName]: Array<BookmarkedItem>
saveData(): void
}
let bookmarksCache: OrderedBookmarks | undefined = undefined
@ -86,34 +95,39 @@ export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefi
}
}
type TraverseCallback = (item: BookmarkedItem, groupPath: string) => boolean | void
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupPath: string) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
const item = collectionRef[idx];
if (callback(item, groupPath)) return;
if ('group' === item.type) recursiveTraversal(item.items, `${groupPath}${groupPath ? '/' : ''}${item.title}`);
if (callback(item, groupsPath)) return;
if ('group' === item.type) recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${item.title}`);
}
};
recursiveTraversal(items, '');
}
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): OrderedBookmarks | undefined => {
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): OrderedBookmarks | undefined => {
console.log(`Populating bookmarks cache with group scope ${bookmarksGroupName}`)
let bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
if (bookmarks) {
if (bookmarksGroupName) {
const bookmarksGroup: BookmarkedGroup|undefined = bookmarks.find(
(item) => item.type === 'group' && item.title === bookmarksGroupName) as BookmarkedGroup
bookmarks = bookmarksGroup ? bookmarksGroup.items : undefined
}
if (bookmarks) {
const orderedBookmarks: OrderedBookmarks = {}
let order: number = 0
const groupNamePrefix: string = bookmarksGroup ? `${bookmarksGroup}/` : ''
const consumeItem = (item: BookmarkedItem, groupPath: string) => {
if (groupNamePrefix && !groupPath.startsWith(groupNamePrefix)) {
return
}
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
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
const isGroup: boolean = item.type === 'group'
if ((isFile && !isAnchor) || 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
const alreadyConsumed = orderedBookmarks[path]
if (!alreadyConsumed) {
@ -121,7 +135,8 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?:
path: path,
order: order++,
file: isFile,
folder: isFile
folder: isFile,
group: isGroup
}
}
}
@ -130,6 +145,7 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?:
return orderedBookmarks
}
}
}
// Result:
// undefined ==> item not found in bookmarks
@ -145,3 +161,57 @@ export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginIns
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
}
// EXPERIMENTAL - operates on internal structures of core Bookmarks plugin
const createBookmarkFileEntry = (path: string): BookmarkedFile => {
return { type: "file", ctime: Date.now(), path: path }
}
const createBookmarkGroupEntry = (title: string): BookmarkedGroup => {
return { type: "group", ctime: Date.now(), items: [], title: title }
}
export const bookmarkFolderItem = (item: TAbstractFile, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
bookmarkSiblings([item], plugin, bookmarksGroup)
}
export const bookmarkSiblings = (siblings: Array<TAbstractFile>, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
let items = plugin[BookmarksPlugin_items_collectionName]
if (siblings.length === 0) return // for sanity
const parentPathComponents: Array<string> = siblings[0].path.split('/')!
parentPathComponents.pop()
if (bookmarksGroup) {
parentPathComponents.unshift(bookmarksGroup)
}
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)
}
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)
}
});
}
export const saveDataAndUpdateBookmarkViews = (plugin: Bookmarks_PluginInstance, app: App) => {
plugin.saveData()
const bookmarksLeafs = app.workspace.getLeavesOfType('bookmarks')
bookmarksLeafs?.forEach((leaf) => {
(leaf.view as any)?.update?.()
})
}

View File

@ -6,3 +6,8 @@ export function isDefined(o: any): boolean {
export function last<T>(o: Array<T>): T | undefined {
return o?.length > 0 ? o[o.length - 1] : undefined
}
export function lastPathComponent(path: string): string {
const pathComponents = (path ?? '').split('/')
return pathComponents.pop()!
}