74 integration with bookmarks core plugin (#84)
* #74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement - added new plugin setting to enable auto-integration with bookmarks - full integration with standard sorting at folder level and at sorting group level - refined support for implicit sorting for bookmarks plugin integration - documentation update (partial, sketchy) - context menu for 'bookmark this' and 'bookmark+siblings' for sorting - order of bookmarked siblings reflects the current sorting in File Explorer, whatever it is (!!!) - handler for 'changed' event of the vault to reflect files/folders locations change in the bookmarks automatically, whenever applicable - adjusted behavior of by-bookmark-comparator to adhere to the multi-level sorting support implemented in #89 and merged recently to this branch. Basically, each comparator is responsible only for its own comparison and should no fallback into other default comparisons - instead it should return 0, which indicates item which are equal from the perspective of the comparator - adjusted bookmarks integration to the merged from master multi-level sorting support and its implications - fix suggested by github code check - advanced performance optimizations: bookmarks cache finetuning, switching to Obsidian sort code if the implicit bookmark-integration sorting spec is in effect for a folder and there are no bookmarked items in the folder - update of treatment of whitespace in parent path extraction: spaces allowed as leading/trailing in path elements - increased coverage of the new functionality with unit tests - basic manual tests done - next step: real-life usage tests
This commit is contained in:
parent
23b50da6cf
commit
fe4f28b46b
|
@ -181,6 +181,48 @@ sorting-spec: |
|
|||
The artificial separator `---+---` defines a sorting group, which will not match any folders or files
|
||||
and is used here to logically separate the series of combined groups into to logical sets
|
||||
|
||||
## Bookmarks plugin integration
|
||||
|
||||
Integration with the __Bookmarks core plugin__ allows for ordering of items via drag & drop in Bookmarks view and reflecting the same order in File Explorer automatically
|
||||
|
||||
TODO: the simple scenario presented on movie
|
||||
|
||||
A separate group of bookmarks is designated by default, to separate from ...
|
||||
|
||||
If at least one item in folder is bookmarked and the auto-enabled, the order of the items becomes managed by the custom sort plugin
|
||||
|
||||
Auto-integration works without any need for sorting configuration files. Under the hood it is equivalent to applying the following global sorting specification:
|
||||
```yaml
|
||||
---
|
||||
sorting-spec: |
|
||||
target-folder: /*
|
||||
bookmarked:
|
||||
< by-bookmarks-order
|
||||
sorting: standard
|
||||
---
|
||||
```
|
||||
|
||||
Auto-integration doesn't apply to folders, for which explicit sorting specification is defined in YAML.
|
||||
In that case, if you want to employ the grouping and/or ordering by bookmarks order, you need to use explicit syntax:
|
||||
|
||||
```yaml
|
||||
---
|
||||
sorting-spec: |
|
||||
target-folder: My folder
|
||||
bookmarked:
|
||||
< by-bookmarks-order
|
||||
---
|
||||
```
|
||||
|
||||
TODO: more instructions plus movie of advanced integration, where bookmarks reflect the folders structure
|
||||
|
||||
Also hints for updating
|
||||
|
||||
A folder is excluded from auto-integration if:
|
||||
- has custom sorting spec
|
||||
- you can (if needed) enable the auto-integration for part of the items
|
||||
- has explicitly applied 'sorting: standard'
|
||||
|
||||
## Matching starred items
|
||||
|
||||
The Obsidian core plugin `Starred` allows the user to 'star' files\
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"jest": "^28.1.1",
|
||||
"monkey-around": "^2.3.0",
|
||||
"obsidian": "^0.15.4",
|
||||
"obsidian-1.4.11": "npm:obsidian@1.4.11",
|
||||
"ts-jest": "^28.0.5",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
FolderItemForSorting,
|
||||
getComparator,
|
||||
getSorterFnFor,
|
||||
getMdata,
|
||||
OS_byCreatedTime,
|
||||
OS_byModifiedTime,
|
||||
OS_byModifiedTimeReverse, SortingLevelId
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import {MetadataCache, Plugin} from "obsidian";
|
||||
|
||||
export enum CustomSortGroupType {
|
||||
Outsiders, // Not belonging to any of other groups
|
||||
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
||||
|
@ -9,6 +7,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
|
||||
}
|
||||
|
||||
|
@ -29,7 +28,9 @@ export enum CustomSortOrder {
|
|||
byMetadataFieldTrueAlphabetical,
|
||||
byMetadataFieldAlphabeticalReverse,
|
||||
byMetadataFieldTrueAlphabeticalReverse,
|
||||
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||
standardObsidian, // whatever user selected in the UI
|
||||
byBookmarkOrder,
|
||||
byBookmarkOrderReverse,
|
||||
default = alphabetical
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
import {
|
||||
CustomSortGroupType,
|
||||
CustomSortOrder,
|
||||
CustomSortSpec
|
||||
} from "./custom-sort-types";
|
||||
import {
|
||||
collectSortingAndGroupingTypes,
|
||||
hasOnlyByBookmarkOrStandardObsidian,
|
||||
HasSortingOrGrouping,
|
||||
ImplicitSortspecForBookmarksIntegration
|
||||
} from "./custom-sort-utils";
|
||||
import {SortingSpecProcessor, SortSpecsCollection} from "./sorting-spec-processor";
|
||||
|
||||
type NM = number
|
||||
|
||||
const getHas = (gTotal?: NM, gBkmrk?: NM, gStar?: NM, gIcon?: NM, sTot?: NM, sBkmrk?: NM, sStd?: NM) => {
|
||||
const has: HasSortingOrGrouping = {
|
||||
grouping: {
|
||||
total: gTotal ||0,
|
||||
byBookmarks: gBkmrk ||0,
|
||||
byStarred: gStar ||0,
|
||||
byIcon: gIcon ||0
|
||||
},
|
||||
sorting: {
|
||||
total: sTot ||0,
|
||||
byBookmarks: sBkmrk ||0,
|
||||
standardObsidian: sStd ||0
|
||||
}
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
||||
describe('hasOnlyByBookmarkOrStandardObsidian and collectSortingAndGroupingTypes', () => {
|
||||
it('should handle empty spec correctly', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined|null = undefined
|
||||
const expectedHas: HasSortingOrGrouping = getHas()
|
||||
const has = collectSortingAndGroupingTypes(spec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(has).toEqual(expectedHas)
|
||||
expect(hasOnly).toBeTruthy()
|
||||
})
|
||||
it('should handle empty spec correctly (null variant)', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined|null = null
|
||||
const expectedHas: HasSortingOrGrouping = getHas()
|
||||
const has = collectSortingAndGroupingTypes(spec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeTruthy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should handle spec with empty orders correctly', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
groups: [
|
||||
{type: CustomSortGroupType.Outsiders, filesOnly: true},
|
||||
{type: CustomSortGroupType.Outsiders}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas()
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeTruthy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should detect not matching default order', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
defaultOrder: CustomSortOrder.default,
|
||||
groups: [
|
||||
{
|
||||
type: CustomSortGroupType.ExactName,
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 1, 0, 0)
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeFalsy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should detect not matching default secondary order', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
defaultSecondaryOrder: CustomSortOrder.default,
|
||||
groups: [
|
||||
{
|
||||
type: CustomSortGroupType.BookmarkedOnly,
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 0)
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeFalsy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should detect not matching order in group', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||
groups: [
|
||||
{
|
||||
type: CustomSortGroupType.ExactName,
|
||||
order: CustomSortOrder.byCreatedTimeReverse
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 3, 1, 1)
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeFalsy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should detect not matching secondary order in group', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||
groups: [
|
||||
{
|
||||
type: CustomSortGroupType.ExactName,
|
||||
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||
secondaryOrder: CustomSortOrder.standardObsidian
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
order: CustomSortOrder.byBookmarkOrder,
|
||||
secondaryOrder: CustomSortOrder.alphabetical
|
||||
}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 6, 3, 2)
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeFalsy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
it('should detect matching orders at all levels', () => {
|
||||
const spec: Partial<CustomSortSpec>|undefined = {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||
groups: [
|
||||
{
|
||||
type: CustomSortGroupType.BookmarkedOnly,
|
||||
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||
secondaryOrder: CustomSortOrder.standardObsidian
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
order: CustomSortOrder.byBookmarkOrder,
|
||||
secondaryOrder: CustomSortOrder.byBookmarkOrderReverse
|
||||
}
|
||||
]
|
||||
}
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 6, 4, 2)
|
||||
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeTruthy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ImplicitSortspecForBookmarksIntegration', () => {
|
||||
it('should correctly be recognized as only bookmark and obsidian standard', () => {
|
||||
const processor: SortingSpecProcessor = new SortingSpecProcessor();
|
||||
const inputTxtArr: Array<string> = ImplicitSortspecForBookmarksIntegration.replace(/\t/gi, '').split('\n')
|
||||
const spec: SortSpecsCollection|null|undefined = processor.parseSortSpecFromText(
|
||||
inputTxtArr,
|
||||
'mock-folder',
|
||||
'custom-name-note.md',
|
||||
null,
|
||||
true
|
||||
)
|
||||
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 1)
|
||||
const has = collectSortingAndGroupingTypes(spec?.sortSpecByPath!['/'])
|
||||
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||
expect(hasOnly).toBeTruthy()
|
||||
expect(has).toEqual(expectedHas)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO - czy tamto sprawdzanie dla itemów w rootowym filderze hasBookmarkInFolder dobrze zadziala
|
|
@ -0,0 +1,80 @@
|
|||
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||
|
||||
// Put here to allow unit tests coverage of this specific implicit sorting spec
|
||||
export const ImplicitSortspecForBookmarksIntegration: string = `
|
||||
target-folder: /*
|
||||
bookmarked:
|
||||
< by-bookmarks-order
|
||||
sorting: standard
|
||||
`
|
||||
|
||||
export interface HasSortingTypes {
|
||||
byBookmarks: number
|
||||
standardObsidian: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface HasGroupingTypes {
|
||||
byBookmarks: number
|
||||
byStarred: number
|
||||
byIcon: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface HasSortingOrGrouping {
|
||||
sorting: HasSortingTypes
|
||||
grouping: HasGroupingTypes
|
||||
}
|
||||
|
||||
export const checkByBookmark = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||
groupType === CustomSortGroupType.BookmarkedOnly && has.grouping.byBookmarks++;
|
||||
(order === CustomSortOrder.byBookmarkOrder || order === CustomSortOrder.byBookmarkOrderReverse) && has.sorting.byBookmarks++;
|
||||
}
|
||||
|
||||
export const checkByStarred = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||
groupType === CustomSortGroupType.StarredOnly && has.grouping.byStarred++;
|
||||
}
|
||||
|
||||
export const checkByIcon = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||
groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++;
|
||||
}
|
||||
|
||||
export const checkStandardObsidian = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||
order === CustomSortOrder.standardObsidian && has.sorting.standardObsidian++;
|
||||
}
|
||||
|
||||
export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => {
|
||||
checkByBookmark(has, order, groupType)
|
||||
checkByStarred(has, order, groupType)
|
||||
checkByIcon(has, order, groupType)
|
||||
checkStandardObsidian(has, order, groupType)
|
||||
|
||||
order !== undefined && has.sorting.total++
|
||||
groupType !== undefined && groupType !== CustomSortGroupType.Outsiders && has.grouping.total++;
|
||||
}
|
||||
|
||||
export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => {
|
||||
const has: HasSortingOrGrouping = {
|
||||
grouping: {
|
||||
byIcon: 0, byStarred: 0, byBookmarks: 0, total: 0
|
||||
},
|
||||
sorting: {
|
||||
byBookmarks: 0, standardObsidian: 0, total: 0
|
||||
}
|
||||
}
|
||||
if (!sortSpec) return has
|
||||
doCheck(has, sortSpec.defaultOrder)
|
||||
doCheck(has, sortSpec.defaultSecondaryOrder)
|
||||
if (sortSpec.groups) {
|
||||
for (let group of sortSpec.groups) {
|
||||
doCheck(has, group.order, group.type)
|
||||
doCheck(has, group.secondaryOrder)
|
||||
}
|
||||
}
|
||||
return has
|
||||
}
|
||||
|
||||
export const hasOnlyByBookmarkOrStandardObsidian = (has: HasSortingOrGrouping): boolean => {
|
||||
return has.sorting.total === has.sorting.standardObsidian + has.sorting.byBookmarks &&
|
||||
has.grouping.total === has.grouping.byBookmarks
|
||||
}
|
|
@ -9,12 +9,24 @@ import {
|
|||
getSorterFnFor,
|
||||
matchGroupRegex,
|
||||
ProcessingContext,
|
||||
sorterByBookmarkOrder,
|
||||
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
|
||||
|
@ -2854,3 +2866,39 @@ describe('sorterByMetadataField', () => {
|
|||
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,0, 'a','b'],
|
||||
[true,undefined,undefined,0, '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,0, 'a','b'],
|
||||
[false,undefined,undefined,0, '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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -9,11 +9,11 @@ import {
|
|||
} from 'obsidian';
|
||||
import {
|
||||
determineStarredStatusOf,
|
||||
Starred_PluginInstance,
|
||||
Starred_PluginInstance
|
||||
} from '../utils/StarredPluginSignature';
|
||||
import {
|
||||
determineIconOf,
|
||||
ObsidianIconFolder_PluginInstance,
|
||||
ObsidianIconFolder_PluginInstance
|
||||
} from '../utils/ObsidianIconFolderPluginSignature'
|
||||
import {
|
||||
CustomSortGroup,
|
||||
|
@ -30,13 +30,16 @@ import {
|
|||
import {
|
||||
expandMacros
|
||||
} from "./macros";
|
||||
import {
|
||||
BookmarksPluginInterface
|
||||
} from "../utils/BookmarksCorePluginSignature";
|
||||
|
||||
export interface ProcessingContext {
|
||||
// For internal transient use
|
||||
plugin?: Plugin // to hand over the access to App instance to the sorting engine
|
||||
_mCache?: MetadataCache
|
||||
starredPluginInstance?: Starred_PluginInstance
|
||||
|
||||
bookmarksPluginInstance?: BookmarksPluginInterface,
|
||||
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
|
||||
}
|
||||
|
||||
|
@ -64,6 +67,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 enum SortingLevelId {
|
||||
|
@ -115,6 +119,23 @@ export const sorterByMetadataField = (reverseOrder?: boolean, trueAlphabetical?:
|
|||
}
|
||||
}
|
||||
|
||||
export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||
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
|
||||
|
||||
return EQUAL_OR_UNCOMPARABLE
|
||||
}
|
||||
}
|
||||
|
||||
const 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),
|
||||
|
@ -132,6 +153,8 @@ const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
|||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forPrimary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
||||
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
|
||||
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
|
||||
|
||||
// This is a fallback entry which should not be used - the getSorterFor() function below should protect against it
|
||||
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||
|
@ -294,6 +317,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
let groupIdx: number
|
||||
let determined: boolean = false
|
||||
let derivedText: string | null | undefined
|
||||
let bookmarkedIdx: number | undefined
|
||||
|
||||
const aFolder: boolean = isFolder(entry)
|
||||
const aFile: boolean = !aFolder
|
||||
|
@ -402,6 +426,14 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
}
|
||||
}
|
||||
break
|
||||
case CustomSortGroupType.BookmarkedOnly:
|
||||
if (ctx?.bookmarksPluginInstance) {
|
||||
const bookmarkOrder: number | undefined = ctx?.bookmarksPluginInstance.determineBookmarkOrder(entry.path)
|
||||
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)
|
||||
|
@ -486,7 +518,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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,6 +536,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
|
||||
|
@ -546,13 +590,33 @@ 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: BookmarksPluginInterface) => {
|
||||
if (!plugin) return
|
||||
|
||||
folderItems.forEach((item) => {
|
||||
const folderDefaultSortRequiresBookmarksOrder: boolean = !!(sortingSpec.defaultOrder && sortOrderNeedsBookmarksOrder(sortingSpec.defaultOrder, sortingSpec.defaultSecondaryOrder))
|
||||
let groupSortRequiresBookmarksOrder: boolean = false
|
||||
if (!folderDefaultSortRequiresBookmarksOrder) {
|
||||
const groupIdx: number | undefined = item.groupIdx
|
||||
if (groupIdx !== undefined) {
|
||||
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
||||
const groupSecondaryOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].secondaryOrder
|
||||
groupSortRequiresBookmarksOrder = sortOrderNeedsBookmarksOrder(groupOrder, groupSecondaryOrder)
|
||||
}
|
||||
}
|
||||
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) {
|
||||
item.bookmarkedIdx = plugin.determineBookmarkOrder(item.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
|
||||
let fileExplorer = this.fileExplorer
|
||||
|
||||
// shallow copy of groups
|
||||
// shallow copy of groups and expand folder-specific macros on them
|
||||
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||
|
||||
// expand folder-specific macros
|
||||
const parentFolderName: string|undefined = this.file.name
|
||||
expandMacros(sortingSpec, parentFolderName)
|
||||
|
||||
|
@ -570,6 +634,10 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
|
|||
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
|
||||
determineFolderDatesIfNeeded(folderItems, sortingSpec)
|
||||
|
||||
if (ctx.bookmarksPluginInstance) {
|
||||
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPluginInstance)
|
||||
}
|
||||
|
||||
const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder)
|
||||
|
||||
folderItems.sort(comparator)
|
||||
|
@ -583,3 +651,41 @@ 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 (folder: TFolder, items: Array<TAbstractFile>, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array<TAbstractFile> {
|
||||
if (sortingSpec) {
|
||||
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
|
||||
|
||||
// shallow copy of groups and expand folder-specific macros on them
|
||||
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||
const parentFolderName: string|undefined = folder.name
|
||||
expandMacros(sortingSpec, parentFolderName)
|
||||
|
||||
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.bookmarksPluginInstance) {
|
||||
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPluginInstance)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import {FolderWildcardMatching} from './folder-matching-rules'
|
||||
import {
|
||||
FolderWildcardMatching
|
||||
} from './folder-matching-rules'
|
||||
|
||||
type SortingSpec = string
|
||||
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
WordInASCIIRegexStr,
|
||||
WordInAnyLanguageRegexStr
|
||||
} from "./matchers";
|
||||
import {SortingSpecProcessor} from "./sorting-spec-processor";
|
||||
|
||||
describe('Plain numbers regexp', () => {
|
||||
let regexp: RegExp;
|
||||
|
|
|
@ -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
|
||||
|
@ -37,6 +45,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 +110,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
|
||||
%
|
||||
|
@ -194,6 +216,29 @@ 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
|
||||
},
|
||||
{
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}
|
||||
],
|
||||
outsidersGroupIdx: 2,
|
||||
targetFoldersPaths: [
|
||||
"folder of bookmarks"
|
||||
]
|
||||
},
|
||||
"Conceptual model": {
|
||||
groups: [{
|
||||
exactText: "Entities",
|
||||
|
|
|
@ -120,6 +120,7 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
|||
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
||||
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
|
||||
}
|
||||
|
||||
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||
|
@ -241,6 +242,8 @@ const HideItemVerboseLexeme: string = '/--hide:'
|
|||
|
||||
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
||||
|
||||
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
|
||||
|
||||
const StarredItemsIndicatorLexeme: string = 'starred:'
|
||||
|
||||
const IconIndicatorLexeme: string = 'with-icon:'
|
||||
|
@ -1600,6 +1603,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 {
|
||||
|
|
290
src/main.ts
290
src/main.ts
|
@ -1,27 +1,38 @@
|
|||
import {
|
||||
apiVersion,
|
||||
App,
|
||||
FileExplorerView,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MetadataCache,
|
||||
Notice,
|
||||
normalizePath,
|
||||
Notice,
|
||||
Platform,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
requireApiVersion,
|
||||
sanitizeHTMLToDom,
|
||||
setIcon,
|
||||
Setting,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
Vault
|
||||
Vault, WorkspaceLeaf
|
||||
} from 'obsidian';
|
||||
import {around} from 'monkey-around';
|
||||
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 {
|
||||
SortingSpecProcessor,
|
||||
SortSpecsCollection
|
||||
} from './custom-sort/sorting-spec-processor';
|
||||
import {
|
||||
CustomSortSpec
|
||||
} from './custom-sort/custom-sort-types';
|
||||
|
||||
import {
|
||||
addIcons,
|
||||
|
@ -33,9 +44,19 @@ import {
|
|||
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
||||
} from "./custom-sort/icons";
|
||||
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
||||
|
||||
import {
|
||||
BookmarksPluginInterface,
|
||||
getBookmarksPlugin,
|
||||
groupNameForPath
|
||||
} from "./utils/BookmarksCorePluginSignature";
|
||||
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
||||
import {lastPathComponent} from "./utils/utils";
|
||||
import {
|
||||
collectSortingAndGroupingTypes,
|
||||
hasOnlyByBookmarkOrStandardObsidian,
|
||||
HasSortingOrGrouping,
|
||||
ImplicitSortspecForBookmarksIntegration
|
||||
} from "./custom-sort/custom-sort-utils";
|
||||
|
||||
interface CustomSortPluginSettings {
|
||||
additionalSortspecFile: string
|
||||
|
@ -43,6 +64,9 @@ interface CustomSortPluginSettings {
|
|||
statusBarEntryEnabled: boolean
|
||||
notificationsEnabled: boolean
|
||||
mobileNotificationsEnabled: boolean
|
||||
automaticBookmarksIntegration: boolean
|
||||
bookmarksContextMenus: boolean
|
||||
bookmarksGroupToConsumeAsOrderingReference: string
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
||||
|
@ -50,7 +74,16 @@ 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,
|
||||
automaticBookmarksIntegration: false,
|
||||
bookmarksContextMenus: false,
|
||||
bookmarksGroupToConsumeAsOrderingReference: 'sortspec'
|
||||
}
|
||||
|
||||
// On API 1.2.x+ enable the bookmarks integration by default
|
||||
const DEFAULT_SETTING_FOR_1_2_0_UP: Partial<CustomSortPluginSettings> = {
|
||||
automaticBookmarksIntegration: true,
|
||||
bookmarksContextMenus: true
|
||||
}
|
||||
|
||||
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
|
||||
|
@ -87,6 +120,16 @@ export default class CustomSortPlugin extends Plugin {
|
|||
this.sortSpecCache = null
|
||||
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
||||
|
||||
if (this.settings.automaticBookmarksIntegration) {
|
||||
this.sortSpecCache = processor.parseSortSpecFromText(
|
||||
ImplicitSortspecForBookmarksIntegration.split('\n'),
|
||||
'System internal path', // Dummy unused value, there are no errors in the internal spec
|
||||
'System internal file', // Dummy unused value, there are no errors in the internal spec
|
||||
this.sortSpecCache,
|
||||
true // Implicit sorting spec generation
|
||||
)
|
||||
}
|
||||
|
||||
Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => {
|
||||
if (failed) return
|
||||
if (file instanceof TFile) {
|
||||
|
@ -198,6 +241,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();
|
||||
|
@ -290,6 +339,131 @@ export default class CustomSortPlugin extends Plugin {
|
|||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
|
||||
if (!this.settings.bookmarksContextMenus) return; // Don't show the context menus at all
|
||||
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (!bookmarksPlugin) return; // Don't show context menu if bookmarks plugin not available and not enabled
|
||||
|
||||
const bookmarkThisMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: bookmark for sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
bookmarksPlugin.bookmarkFolderItem(file)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
const unbookmarkThisMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: UNbookmark from sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
const bookmarkAllMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: bookmark+siblings for sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent, bookmarksPlugin)
|
||||
bookmarksPlugin.bookmarkSiblings(orderedChildren)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
const unbookmarkAllMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: UNbookmark+all siblings from sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
const orderedChildren: Array<TAbstractFile> = file.parent.children.map((entry: TFile | TFolder) => entry)
|
||||
bookmarksPlugin.unbookmarkSiblings(orderedChildren)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const itemAlreadyBookmarkedForSorting: boolean = bookmarksPlugin.isBookmarkedForSorting(file)
|
||||
if (!itemAlreadyBookmarkedForSorting) {
|
||||
menu.addItem(bookmarkThisMenuItem)
|
||||
} else {
|
||||
menu.addItem(unbookmarkThisMenuItem)
|
||||
}
|
||||
menu.addItem(bookmarkAllMenuItem)
|
||||
menu.addItem(unbookmarkAllMenuItem)
|
||||
})
|
||||
)
|
||||
|
||||
if (requireApiVersion('1.4.11')) {
|
||||
this.registerEvent(
|
||||
// "files-menu" event was exposed in 1.4.11
|
||||
// @ts-ignore
|
||||
app.workspace.on("files-menu", (menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf) => {
|
||||
if (!this.settings.bookmarksContextMenus) return; // Don't show the context menus at all
|
||||
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (!bookmarksPlugin) return; // Don't show context menu if bookmarks plugin not available and not enabled
|
||||
|
||||
const bookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: bookmark selected for sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
files.forEach((file) => {
|
||||
bookmarksPlugin.bookmarkFolderItem(file)
|
||||
})
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
const unbookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||
item.setTitle('Custom sort: UNbookmark selected from sorting.');
|
||||
item.setIcon('hashtag');
|
||||
item.onClick(() => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
files.forEach((file) => {
|
||||
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||
})
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
menu.addItem(bookmarkSelectedMenuItem)
|
||||
menu.addItem(unbookmarkSelectedMenuItem)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.registerEvent(
|
||||
app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
bookmarksPlugin.updateSortingBookmarksAfterItemRenamed(file, oldPath)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
})
|
||||
)
|
||||
app.vault.on("delete", (file: TAbstractFile) => {
|
||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
if (bookmarksPlugin) {
|
||||
bookmarksPlugin.updateSortingBookmarksAfterItemDeleted(file)
|
||||
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerCommands() {
|
||||
|
@ -328,12 +502,12 @@ export default class CustomSortPlugin extends Plugin {
|
|||
return sortSpec
|
||||
}
|
||||
|
||||
createProcessingContextForSorting(): ProcessingContext {
|
||||
createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext {
|
||||
const ctx: ProcessingContext = {
|
||||
_mCache: app.metadataCache,
|
||||
starredPluginInstance: getStarredPlugin(),
|
||||
|
||||
iconFolderPluginInstance: getIconFolderPlugin(),
|
||||
starredPluginInstance: has.grouping.byStarred ? getStarredPlugin() : undefined,
|
||||
bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined,
|
||||
iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin() : undefined,
|
||||
plugin: this
|
||||
}
|
||||
return ctx
|
||||
|
@ -365,8 +539,18 @@ export default class CustomSortPlugin extends Plugin {
|
|||
const folder: TFolder = this.file
|
||||
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
|
||||
|
||||
// Performance optimization
|
||||
// Primary intention: when the implicit bookmarks integration is enabled, remain on std Obsidian, if no need to involve bookmarks
|
||||
let has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
||||
if (hasOnlyByBookmarkOrStandardObsidian(has)) {
|
||||
const bookmarksPlugin: BookmarksPluginInterface|undefined = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference, false, true)
|
||||
if ( !bookmarksPlugin?.bookmarksIncludeItemsInFolder(folder.path)) {
|
||||
sortSpec = null
|
||||
}
|
||||
}
|
||||
|
||||
if (sortSpec) {
|
||||
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting());
|
||||
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting(has));
|
||||
} else {
|
||||
return old.call(this, ...args);
|
||||
}
|
||||
|
@ -380,6 +564,24 @@ export default class CustomSortPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
orderedFolderItemsForBookmarking(folder: TFolder, bookmarksPlugin: BookmarksPluginInterface): 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
|
||||
|
||||
const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
||||
|
||||
return sortFolderItemsForBookmarking(
|
||||
folder,
|
||||
folder.children,
|
||||
sortSpec,
|
||||
this.createProcessingContextForSorting(has),
|
||||
uiSortOrder
|
||||
)
|
||||
}
|
||||
|
||||
// Credits go to https://github.com/nothingislost/obsidian-bartender
|
||||
getFileExplorer(): FileExplorerView | undefined {
|
||||
let fileExplorer: FileExplorerView | undefined = app.workspace.getLeavesOfType("file-explorer")?.first()
|
||||
|
@ -397,7 +599,12 @@ export default class CustomSortPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
const data: any = await this.loadData() || {}
|
||||
const isFreshInstall: boolean = Object.keys(data).length === 0
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
if (requireApiVersion('1.2.0')) {
|
||||
this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP)
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
@ -405,6 +612,10 @@ export default class CustomSortPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
const pathToFlatString = (path: string): string => {
|
||||
return path.replace(/\//g,'_').replace(/\\/g, '_')
|
||||
}
|
||||
|
||||
class CustomSortSettingTab extends PluginSettingTab {
|
||||
plugin: CustomSortPlugin;
|
||||
|
||||
|
@ -492,5 +703,60 @@ class CustomSortSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.mobileNotificationsEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
containerEl.createEl('h2', {text: 'Bookmarks integration'});
|
||||
const bookmarksIntegrationDescription: DocumentFragment = sanitizeHTMLToDom(
|
||||
'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'
|
||||
+ ' <pre style="display: inline;">sorting-spec:</pre> configurations and they can nicely cooperate.'
|
||||
+ '<br>'
|
||||
+ '<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 '
|
||||
+ "'<i>" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "</i>' "
|
||||
+ 'and you can change the group name in the configuration field below.'
|
||||
+ '<br>'
|
||||
+ 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.</p>'
|
||||
+ '<p>More information on this functionality in the '
|
||||
+ '<a href="https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/manual.md#bookmarks-plugin-integration">'
|
||||
+ 'manual</a> of this custom-sort plugin.'
|
||||
+ '</p>'
|
||||
)
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)')
|
||||
.setDesc(bookmarksIntegrationDescription)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.automaticBookmarksIntegration)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.automaticBookmarksIntegration = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Name of the group in Bookmarks from which to read the order of items')
|
||||
.setDesc('See above.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('e.g. Group for sorting')
|
||||
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||
.onChange(async (value) => {
|
||||
value = groupNameForPath(value.trim()).trim()
|
||||
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value ? pathToFlatString(normalizePath(value)) : '';
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
const bookmarksIntegrationContextMenusDescription: DocumentFragment = sanitizeHTMLToDom(
|
||||
'Enable <i>Custom-sort: bookmark for sorting</i> and <i>Custom-sort: bookmark+siblings for sorting.</i> entries '
|
||||
+ 'in context menu in File Explorer'
|
||||
)
|
||||
new Setting(containerEl)
|
||||
.setName('Context menus for Bookmarks integration')
|
||||
.setDesc(bookmarksIntegrationContextMenusDescription)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.bookmarksContextMenus)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.bookmarksContextMenus = value;
|
||||
await this.plugin.saveSettings();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
Integration with Bookmarks core plugin:
|
||||
- support two approaches _at the same time_:
|
||||
- (A) structured bookmarks inside a dedicated bookmarks group, and
|
||||
- (B) a flat list of bookmarks inside the dedicated bookmarks group
|
||||
|
||||
For (A):
|
||||
- preferred
|
||||
- a folder is represented by a group in bookmarks
|
||||
- a file is represented by a file-with-block
|
||||
- this also applied to non-md files, like jpg and others
|
||||
- guarantees _'hiding'_ the bookmarks-for-sorting from regular bookmarks usage scenarios
|
||||
- bookmark entries for sorting are encapsulated in the dedicated group
|
||||
- they don't interfere with bookmarking of files and folders via standard bookmarking
|
||||
- only exact location of file bookmark / group matches for sorting order in file explorer
|
||||
- the contextual bookmark menus always work in (A) mode
|
||||
- the contextual menus create / modify the bookmarks structure on-the-fly
|
||||
|
||||
For (B):
|
||||
- discouraged, yet supported (exception for some edge cases)
|
||||
- typically a result of manual bookmarks management
|
||||
- for small number of items seems reasonable
|
||||
- for flat vaults it could look same as for (A)
|
||||
- groups don't have a 'path' attribute, their path is determined by their location
|
||||
- bookmarked folders represent folders if inside the bookmarks group for sorting
|
||||
- yet in this way they interfere with regular bookmarks scenario
|
||||
- file bookmarks work correctly in non-interfering way thanks to the _'artificial block reference'_
|
||||
- file bookmarks not having the _'artificial block ref'_ work as well
|
||||
- if they are in the designated bookmarks group
|
||||
- if there isn't a duplicate, which has the _'artificial block ref'_
|
||||
- yet in this way they interfere with regular bookmarks scenario
|
||||
|
||||
-[ ] TODO: review again the 'item moved' and 'item deleted' scenarios (they look ok, check if they don't delete/move too much)
|
||||
- [x] fundamental question 1: should 'move' create a bookmark entry/structure if it is not covered by bookmarks?
|
||||
- Answer: the moved item is removed from bookmarks. If it is a group with descendants not transparent for sorting,
|
||||
it is renamed to become transparent for sorting.
|
||||
By design, the order of items is property of the parent folder (the container) and not the items
|
||||
- [x] fundamental question 2: should 'move' create a bookmark entry if moved item was not bookmarked, yet is moved to a folder covered by bookmarks?
|
||||
- Answer: same as for previous point.
|
||||
- [x] review from (A) and (B) perspective
|
||||
- Answer: scenario (A) is fully handled by 'item moved' and 'item deleted'.
|
||||
scenario (B) is partially handled for 'item moved'. Details to be read from code (too complex to cover here)
|
||||
- [x] consider deletion of item outside of bookmarks sorting container group
|
||||
Answer: bookmark items outside of bookmarks sorting container are not manipulated by custom-sort plugin
|
||||
to not interfere with standard Bookmarks scenarios
|
||||
- [x] consider moving an item outside of bookmarks group
|
||||
- Answer: question not relevant. Items are moved in file explorer and bookmarks only reflect that, if needed.
|
||||
Hence there is no concept of 'moving an item outside of bookmarks group' - bookmarks group only exists in bookmarks
|
||||
- [x] edge case: bookmarked item is a group, the deleted/moved is a file, not a folder --> what to do?
|
||||
- Answer: for moved files, only file bookmarks are scanned (and handles), for moved folders, only groups are scanned (and handled).
|
||||
- [x] delete all instances at any level of bookmarks structure in 'delete' handler
|
||||
- Answer: only instances of (A) or (B) are deleted. Items outside of bookmarks container for sorting or
|
||||
in invalid locations in bookmarks hierarchy are ignored
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,623 @@
|
|||
import {
|
||||
InstalledPlugin,
|
||||
PluginInstance,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder
|
||||
} from "obsidian";
|
||||
import {
|
||||
extractParentFolderPath,
|
||||
lastPathComponent
|
||||
} from "./utils";
|
||||
import {Arr} from "tern";
|
||||
import * as process from "process";
|
||||
|
||||
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
||||
|
||||
const BookmarksPlugin_items_collectionName = 'items'
|
||||
|
||||
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 BookmarkItemSuperset {
|
||||
path: Path
|
||||
title?: string
|
||||
ctime: number
|
||||
subpath?: string // Anchor within the file (heading and/or block ref)
|
||||
}
|
||||
|
||||
interface BookmarkWithPath extends Pick<BookmarkItemSuperset, 'path'> {
|
||||
}
|
||||
|
||||
interface BookmarkedFile extends BookmarkItemSuperset {
|
||||
type: 'file'
|
||||
}
|
||||
|
||||
interface BookmarkedFolder extends Omit<BookmarkItemSuperset, 'subpath'> {
|
||||
type: 'folder'
|
||||
}
|
||||
|
||||
interface BookmarkedGroup extends Omit<BookmarkItemSuperset, 'subpath'|'path'> {
|
||||
type: 'group'
|
||||
items: Array<BookmarkedItem>
|
||||
}
|
||||
|
||||
export type BookmarkedItemPath = string
|
||||
|
||||
export interface OrderedBookmarkedItemWithMetadata {
|
||||
isGroup?: boolean
|
||||
path: BookmarkedItemPath
|
||||
hasSortingIndicator?: boolean
|
||||
order: number
|
||||
bookmarkPathMatches?: boolean
|
||||
}
|
||||
|
||||
export type OrderedBookmarkedItem = Pick<OrderedBookmarkedItemWithMetadata, 'order'>
|
||||
export type Order = number
|
||||
|
||||
export interface OrderedBookmarks {
|
||||
[key: BookmarkedItemPath]: Order
|
||||
}
|
||||
|
||||
export interface OrderedBookmarksWithMetadata {
|
||||
[key: BookmarkedItemPath]: OrderedBookmarkedItemWithMetadata
|
||||
}
|
||||
|
||||
interface Bookmarks_PluginInstance extends PluginInstance {
|
||||
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
|
||||
[BookmarksPlugin_items_collectionName]: Array<BookmarkedItem>
|
||||
saveData(): void
|
||||
onItemsChanged(saveData: boolean): void
|
||||
}
|
||||
|
||||
export interface BookmarksPluginInterface {
|
||||
determineBookmarkOrder(path: string): number|undefined
|
||||
bookmarkFolderItem(item: TAbstractFile): void
|
||||
unbookmarkFolderItem(item: TAbstractFile): void
|
||||
saveDataAndUpdateBookmarkViews(updateBookmarkViews: boolean): void
|
||||
bookmarkSiblings(siblings: Array<TAbstractFile>, inTheTop?: boolean): void
|
||||
unbookmarkSiblings(siblings: Array<TAbstractFile>): void
|
||||
updateSortingBookmarksAfterItemRenamed(renamedItem: TAbstractFile, oldPath: string): void
|
||||
updateSortingBookmarksAfterItemDeleted(deletedItem: TAbstractFile): void
|
||||
isBookmarkedForSorting(item: TAbstractFile): boolean
|
||||
|
||||
// To support performance optimization
|
||||
bookmarksIncludeItemsInFolder(folderPath: string): boolean
|
||||
}
|
||||
|
||||
const checkSubtreeForOnlyTransparentGroups = (items: Array<BookmarkedItem>): boolean => {
|
||||
if (!items || items?.length === 0) return true
|
||||
for (let it of items) {
|
||||
if (it.type !== 'group' || !it.title || !isGroupTransparentForSorting(it.title)) {
|
||||
return false
|
||||
}
|
||||
// it is a group transparent for sorting
|
||||
const isEmptyOrTransparent: boolean = checkSubtreeForOnlyTransparentGroups(it.items)
|
||||
if (!isEmptyOrTransparent) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants = (group: BookmarkedGroup): boolean => {
|
||||
return checkSubtreeForOnlyTransparentGroups(group.items)
|
||||
}
|
||||
|
||||
class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||
|
||||
plugin: Bookmarks_PluginInstance|undefined
|
||||
groupNameForSorting: string|undefined
|
||||
|
||||
constructor () {
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// Parameterless invocation enforces cache population, if empty
|
||||
determineBookmarkOrder = (path?: string): Order | undefined => {
|
||||
if (!bookmarksCache) {
|
||||
[bookmarksCache, bookmarksFoldersCoverage] = getOrderedBookmarks(this.plugin!, this.groupNameForSorting)
|
||||
bookmarksCacheTimestamp = Date.now()
|
||||
}
|
||||
|
||||
if (path && path.length > 0) {
|
||||
const bookmarkedItemPosition: Order | undefined = bookmarksCache?.[path]
|
||||
|
||||
return (bookmarkedItemPosition && bookmarkedItemPosition > 0) ? bookmarkedItemPosition : undefined
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
bookmarkFolderItem = (item: TAbstractFile) => {
|
||||
this.bookmarkSiblings([item], true)
|
||||
}
|
||||
|
||||
unbookmarkFolderItem = (item: TAbstractFile) => {
|
||||
this.unbookmarkSiblings([item])
|
||||
}
|
||||
|
||||
saveDataAndUpdateBookmarkViews = (updateBookmarkViews: boolean = true) => {
|
||||
this.plugin!.onItemsChanged(true)
|
||||
if (updateBookmarkViews) {
|
||||
const bookmarksLeafs = app.workspace.getLeavesOfType('bookmarks')
|
||||
bookmarksLeafs?.forEach((leaf) => {
|
||||
(leaf.view as any)?.update?.()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bookmarkSiblings = (siblings: Array<TAbstractFile>, inTheTop?: boolean) => {
|
||||
if (siblings.length === 0) return // for sanity
|
||||
|
||||
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
|
||||
siblings[0].path,
|
||||
CreateIfMissing,
|
||||
this.plugin!,
|
||||
this.groupNameForSorting
|
||||
)
|
||||
|
||||
if (bookmarksContainer) { // for sanity, the group should be always created if missing
|
||||
siblings.forEach((aSibling) => {
|
||||
const siblingName = lastPathComponent(aSibling.path)
|
||||
const groupTransparentForSorting = bookmarksContainer.items.find((it) => (
|
||||
it.type === 'group' && groupNameForPath(it.title||'') === siblingName && isGroupTransparentForSorting(it.title)
|
||||
))
|
||||
if (groupTransparentForSorting) {
|
||||
// got a group transparent for sorting
|
||||
groupTransparentForSorting.title = groupNameForPath(groupTransparentForSorting.title||'')
|
||||
} else 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);
|
||||
if (inTheTop) {
|
||||
bookmarksContainer.items.unshift(newEntry)
|
||||
} else {
|
||||
bookmarksContainer.items.push(newEntry)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unbookmarkSiblings = (siblings: Array<TAbstractFile>) => {
|
||||
if (siblings.length === 0) return // for sanity
|
||||
|
||||
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
|
||||
siblings[0].path,
|
||||
DontCreateIfMissing,
|
||||
this.plugin!,
|
||||
this.groupNameForSorting
|
||||
)
|
||||
|
||||
if (bookmarksContainer) { // for sanity
|
||||
const bookmarkedItemsToRemove: Array<BookmarkedItem> = []
|
||||
siblings.forEach((aSibling) => {
|
||||
const siblingName = lastPathComponent(aSibling.path)
|
||||
const aGroup = bookmarksContainer.items.find(
|
||||
(it) => (it.type === 'group' && groupNameForPath(it.title||'') === siblingName)
|
||||
)
|
||||
if (aGroup) {
|
||||
const canBeRemoved = bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup as BookmarkedGroup)
|
||||
if (canBeRemoved) {
|
||||
bookmarksContainer.items.remove(aGroup)
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||
} else {
|
||||
if (!isGroupTransparentForSorting(aGroup.title)) {
|
||||
aGroup.title = groupNameTransparentForSorting(aGroup.title||'')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const aFileOrFolder = bookmarksContainer.items.find(
|
||||
(it) => ((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path)
|
||||
)
|
||||
if (aFileOrFolder) {
|
||||
bookmarksContainer.items.remove(aFileOrFolder)
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateSortingBookmarksAfterItemRenamed = (renamedItem: TAbstractFile, oldPath: string): void => {
|
||||
updateSortingBookmarksAfterItemRenamed(this.plugin!, renamedItem, oldPath, this.groupNameForSorting)
|
||||
}
|
||||
|
||||
updateSortingBookmarksAfterItemDeleted = (deletedItem: TAbstractFile): void => {
|
||||
updateSortingBookmarksAfterItemDeleted(this.plugin!, deletedItem, this.groupNameForSorting)
|
||||
}
|
||||
|
||||
isBookmarkedForSorting = (item: TAbstractFile): boolean => {
|
||||
const itemsCollection: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(item.path, DontCreateIfMissing, this.plugin!, this.groupNameForSorting)
|
||||
if (itemsCollection) {
|
||||
if (item instanceof TFile) {
|
||||
return itemsCollection.items?.some((bkmrk) => bkmrk.type === 'file' && bkmrk.path === item.path)
|
||||
} else {
|
||||
const folderName: string = lastPathComponent(item.path)
|
||||
return itemsCollection.items?.some((bkmrk) =>
|
||||
(bkmrk.type === 'group' && bkmrk.title === folderName) ||
|
||||
(bkmrk.type === 'folder' && bkmrk.path === item.path)
|
||||
)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
bookmarksIncludeItemsInFolder = (folderPath: string): boolean => {
|
||||
return !! bookmarksFoldersCoverage?.[folderPath]
|
||||
}
|
||||
}
|
||||
|
||||
export const BookmarksCorePluginId: string = 'bookmarks'
|
||||
|
||||
export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean, ensureCachePopulated?: boolean): BookmarksPluginInterface | undefined => {
|
||||
invalidateExpiredBookmarksCache(forceFlushCache)
|
||||
const installedBookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId)
|
||||
if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) {
|
||||
const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance
|
||||
// defensive programming, in case Obsidian changes its internal APIs
|
||||
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function' &&
|
||||
Array.isArray(bookmarksPluginInstance?.[BookmarksPlugin_items_collectionName])) {
|
||||
bookmarksPlugin.plugin = bookmarksPluginInstance
|
||||
bookmarksPlugin.groupNameForSorting = bookmarksGroupName
|
||||
if (ensureCachePopulated && !bookmarksCache) {
|
||||
bookmarksPlugin.determineBookmarkOrder()
|
||||
}
|
||||
return bookmarksPlugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cache can outlive the wrapper instances
|
||||
let bookmarksCache: OrderedBookmarks | undefined = undefined
|
||||
let bookmarksCacheTimestamp: number | undefined = undefined
|
||||
type FolderPath = string
|
||||
type FoldersCoverage = { [key: FolderPath]: boolean }
|
||||
let bookmarksFoldersCoverage: FoldersCoverage | undefined = undefined
|
||||
|
||||
const bookmarksPlugin: BookmarksPluginWrapper = new BookmarksPluginWrapper()
|
||||
|
||||
const CacheExpirationMilis = 1000 // One second seems to be reasonable
|
||||
|
||||
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
|
||||
bookmarksFoldersCoverage = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
|
||||
|
||||
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsumeItem: TraverseCallback) => {
|
||||
if (!Array.isArray(items)) return
|
||||
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
|
||||
if (!Array.isArray(collection)) return
|
||||
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
|
||||
const item = collectionRef[idx];
|
||||
if (callbackConsumeItem(item, groupsPath)) return;
|
||||
if ('group' === item.type) {
|
||||
const groupNameToUseInPath: string = groupNameForPath(item.title || '')
|
||||
recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${groupNameToUseInPath}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
recursiveTraversal(items, '');
|
||||
}
|
||||
|
||||
const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-'
|
||||
|
||||
const ROOT_FOLDER_PATH = '/'
|
||||
|
||||
const TRANSPARENT_FOR_SORTING_PREFIX = '\\\\'
|
||||
|
||||
const isGroupTransparentForSorting = (name?: string): boolean => {
|
||||
return !!name?.startsWith(TRANSPARENT_FOR_SORTING_PREFIX)
|
||||
}
|
||||
|
||||
const groupNameTransparentForSorting = (name: string): string => {
|
||||
return isGroupTransparentForSorting(name) ? name : `${TRANSPARENT_FOR_SORTING_PREFIX}${name}`
|
||||
}
|
||||
|
||||
export const groupNameForPath = (name: string): string => {
|
||||
return isGroupTransparentForSorting(name) ? name.substring(TRANSPARENT_FOR_SORTING_PREFIX.length) : name
|
||||
}
|
||||
|
||||
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): [OrderedBookmarks, FoldersCoverage] | [undefined, undefined] => {
|
||||
let bookmarksItems: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_items_collectionName]
|
||||
let bookmarksCoveredFolders: FoldersCoverage = {}
|
||||
if (bookmarksItems && Array.isArray(bookmarksItems)) {
|
||||
if (bookmarksGroupName) {
|
||||
// scanning only top level items because by design 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 (bookmarksItems) {
|
||||
const orderedBookmarksWithMetadata: OrderedBookmarksWithMetadata = {}
|
||||
let order: number = 1 // Intentionally start > 0 to allow easy check: if (order) ...
|
||||
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
|
||||
if ('group' === item.type) {
|
||||
if (!isGroupTransparentForSorting(item.title)) {
|
||||
const path: string = `${parentGroupsPath}${parentGroupsPath ? '/' : ''}${item.title}`
|
||||
const alreadyConsumed = orderedBookmarksWithMetadata[path]
|
||||
if (alreadyConsumed) {
|
||||
if (alreadyConsumed.isGroup) return // Defensive programming
|
||||
if (alreadyConsumed.hasSortingIndicator) return
|
||||
}
|
||||
|
||||
orderedBookmarksWithMetadata[path] = {
|
||||
path: path,
|
||||
order: order++,
|
||||
isGroup: true
|
||||
}
|
||||
}
|
||||
} else if ('file' === item.type || 'folder' === item.type) {
|
||||
const itemWithPath = (item as BookmarkWithPath)
|
||||
const itemFile = 'file' === item.type ? (item as BookmarkedFile) : undefined
|
||||
const alreadyConsumed = orderedBookmarksWithMetadata[itemWithPath.path]
|
||||
const hasSortingIndicator: boolean|undefined = itemFile ? itemFile.subpath === ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR : undefined
|
||||
const parentFolderPathOfBookmarkedItem = extractParentFolderPath(itemWithPath.path)
|
||||
const bookmarkPathMatches: boolean = parentGroupsPath === parentFolderPathOfBookmarkedItem
|
||||
const bookmarkPathIsRoot: boolean = !(parentGroupsPath?.length > 0)
|
||||
|
||||
// Bookmarks not in root (group) or in matching path are ignored
|
||||
if (!bookmarkPathMatches && !bookmarkPathIsRoot) return
|
||||
|
||||
// For bookmarks in root or in matching path, apply the prioritized duplicate elimination logic
|
||||
if (alreadyConsumed) {
|
||||
if (hasSortingIndicator) {
|
||||
if (alreadyConsumed.hasSortingIndicator && alreadyConsumed.bookmarkPathMatches) return
|
||||
if (alreadyConsumed.hasSortingIndicator && !bookmarkPathMatches) return
|
||||
} else { // no sorting indicator on new
|
||||
if (alreadyConsumed.hasSortingIndicator) return
|
||||
if (!bookmarkPathMatches || alreadyConsumed.bookmarkPathMatches || alreadyConsumed.isGroup) return
|
||||
}
|
||||
}
|
||||
|
||||
orderedBookmarksWithMetadata[itemWithPath.path] = {
|
||||
path: itemWithPath.path,
|
||||
order: order++,
|
||||
isGroup: false,
|
||||
bookmarkPathMatches: bookmarkPathMatches,
|
||||
hasSortingIndicator: hasSortingIndicator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseBookmarksCollection(bookmarksItems, consumeItem)
|
||||
|
||||
const orderedBookmarks: OrderedBookmarks = {}
|
||||
|
||||
for (let path in orderedBookmarksWithMetadata) {
|
||||
orderedBookmarks[path] = orderedBookmarksWithMetadata[path].order
|
||||
const parentFolderPath: Path = extractParentFolderPath(path)
|
||||
bookmarksCoveredFolders[parentFolderPath.length > 0 ? parentFolderPath : ROOT_FOLDER_PATH] = true
|
||||
}
|
||||
return [orderedBookmarks, bookmarksCoveredFolders]
|
||||
}
|
||||
}
|
||||
return [undefined, undefined]
|
||||
}
|
||||
|
||||
const createBookmarkFileEntry = (path: string): BookmarkedFile => {
|
||||
// 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 => {
|
||||
return { type: "group", ctime: Date.now(), items: [], title: title }
|
||||
}
|
||||
|
||||
export interface BookmarkedParentFolder {
|
||||
pathOfGroup?: Path // undefined when the container is the root of bookmarks
|
||||
group?: BookmarkedGroup // undefined when the item is at root level of bookmarks
|
||||
items: Array<BookmarkedItem> // reference to group.items or to root collection of bookmarks
|
||||
}
|
||||
|
||||
const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: boolean, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): BookmarkedParentFolder|undefined => {
|
||||
let items = plugin?.[BookmarksPlugin_items_collectionName]
|
||||
|
||||
if (!Array.isArray(items)) return undefined
|
||||
|
||||
if (!itemPath || !itemPath.trim()) return undefined // for sanity
|
||||
|
||||
const parentPath: string = extractParentFolderPath(itemPath)
|
||||
|
||||
const parentPathComponents: Array<string> = parentPath ? parentPath.split('/')! : []
|
||||
|
||||
if (bookmarksGroup) {
|
||||
parentPathComponents.unshift(bookmarksGroup)
|
||||
}
|
||||
|
||||
let group: BookmarkedGroup|undefined = undefined
|
||||
|
||||
parentPathComponents.forEach((pathSegment, index) => {
|
||||
group = items.find((it) => it.type === 'group' && groupNameForPath(it.title||'') === pathSegment) as BookmarkedGroup
|
||||
if (!group) {
|
||||
if (createIfMissing) {
|
||||
const theSortingBookmarksContainerGroup = (bookmarksGroup && index === 0)
|
||||
const groupName: string = theSortingBookmarksContainerGroup ? pathSegment : groupNameTransparentForSorting(pathSegment)
|
||||
group = createBookmarkGroupEntry(groupName)
|
||||
items.push(group)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
items = group.items
|
||||
})
|
||||
|
||||
return {
|
||||
items: items,
|
||||
group: group,
|
||||
pathOfGroup: parentPath
|
||||
}
|
||||
}
|
||||
|
||||
const CreateIfMissing = true
|
||||
const DontCreateIfMissing = false
|
||||
|
||||
const renameGroup = (group: BookmarkedGroup, newName: string, makeTransparentForSorting: boolean|undefined) => {
|
||||
if (makeTransparentForSorting === true) {
|
||||
group.title = groupNameTransparentForSorting(newName)
|
||||
} else if (makeTransparentForSorting === false) {
|
||||
group.title = newName
|
||||
} else { // no transparency status, retain the status as-is
|
||||
group.title = isGroupTransparentForSorting(group.title) ? groupNameTransparentForSorting(newName) : newName
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupBookmarkTreeFromTransparentEmptyGroups = (parentGroup: BookmarkedParentFolder|undefined, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
|
||||
|
||||
if (!parentGroup) return // invalid invocation - exit
|
||||
if (!parentGroup.group) return // root folder of the bookmarks - do not touch items in root folder
|
||||
|
||||
if (checkSubtreeForOnlyTransparentGroups(parentGroup.items)) {
|
||||
parentGroup.group.items = []
|
||||
|
||||
const parentContainerOfGroup = findGroupForItemPathInBookmarks(
|
||||
parentGroup.pathOfGroup || '',
|
||||
DontCreateIfMissing,
|
||||
plugin,
|
||||
bookmarksGroup
|
||||
)
|
||||
if (parentContainerOfGroup) {
|
||||
parentContainerOfGroup.group?.items?.remove(parentGroup.group)
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(parentContainerOfGroup, plugin, bookmarksGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => {
|
||||
|
||||
if (renamedItem.path === oldPath) return; // sanity
|
||||
|
||||
const aFolder: boolean = renamedItem instanceof TFolder
|
||||
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
|
||||
|
||||
// file renames are handled automatically by Obsidian in bookmarks, no need for additional actions
|
||||
if (aFile && renamed) return
|
||||
|
||||
const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(oldPath, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||
|
||||
if (!originalContainer) return;
|
||||
|
||||
const item: BookmarkedItem|undefined = aFolder ?
|
||||
originalContainer.items.find((it) => (
|
||||
it.type === 'group' && groupNameForPath(it.title||'') === oldName
|
||||
))
|
||||
: // aFile
|
||||
originalContainer.items.find((it) => (
|
||||
it.type === 'file' && it.path === renamedItem.path
|
||||
))
|
||||
|
||||
if (!item) return;
|
||||
|
||||
// The renamed/moved item was located in bookmarks, actions depend on item type
|
||||
if (aFile) {
|
||||
if (moved) { // sanity
|
||||
originalContainer.group?.items.remove(item)
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||
}
|
||||
} else { // a group
|
||||
const aGroup: BookmarkedGroup = item as BookmarkedGroup
|
||||
|
||||
if (bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup)) {
|
||||
if (moved) { // sanity
|
||||
originalContainer.group?.items.remove(aGroup)
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||
} else if (renamed) {
|
||||
renameGroup(aGroup, newName, undefined)
|
||||
}
|
||||
} else { // group has some descendants not transparent for sorting
|
||||
if (moved) {
|
||||
originalContainer.group?.items.remove(aGroup)
|
||||
const targetContainer: BookmarkedParentFolder | undefined = findGroupForItemPathInBookmarks(renamedItem.path, CreateIfMissing, plugin, bookmarksGroup)
|
||||
if (targetContainer) {
|
||||
targetContainer.group?.items.push(aGroup)
|
||||
// the group in new location becomes by design transparent for sorting.
|
||||
// The sorting order is a property of the parent folder, not the item itself
|
||||
renameGroup(aGroup, groupNameForPath(aGroup.title||''), true)
|
||||
}
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||
}
|
||||
|
||||
if (renamed) {
|
||||
// unrealistic scenario when a folder is moved and renamed at the same time
|
||||
renameGroup(aGroup, newName, undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance, deletedItem: TAbstractFile, bookmarksGroup?: string) => {
|
||||
|
||||
// Obsidian automatically deletes all bookmark instances of a file, nothing to be done here
|
||||
if (deletedItem instanceof TFile) return;
|
||||
|
||||
let items = plugin[BookmarksPlugin_items_collectionName]
|
||||
|
||||
if (!Array.isArray(items)) return
|
||||
|
||||
const aFolder: boolean = deletedItem instanceof TFolder
|
||||
const aFile: boolean = !aFolder
|
||||
|
||||
// Delete all instances of deleted item from two handled locations:
|
||||
// - in bookmark groups hierarchy matching the item path in file explorer
|
||||
// - in the bookmark group designated as container for bookmarks (immediate children)
|
||||
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(deletedItem.path, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||
const itemInRootFolder = !!extractParentFolderPath(deletedItem.path)
|
||||
const bookmarksRootContainer: BookmarkedParentFolder|undefined =
|
||||
(bookmarksGroup && !itemInRootFolder) ? findGroupForItemPathInBookmarks('intentionally-in-root-path', DontCreateIfMissing, plugin, bookmarksGroup) : undefined
|
||||
|
||||
if (!bookmarksContainer && !bookmarksRootContainer) return;
|
||||
|
||||
[bookmarksContainer, bookmarksRootContainer].forEach((container) => {
|
||||
const bookmarkEntriesToRemove: Array<BookmarkedItem> = []
|
||||
container?.items.forEach((it) => {
|
||||
if (aFolder && it.type === 'group' && groupNameForPath(it.title||'') === deletedItem.name) {
|
||||
bookmarkEntriesToRemove.push(it)
|
||||
}
|
||||
if (aFile && it.type === 'file' && it.path === deletedItem.path) {
|
||||
bookmarkEntriesToRemove.push(it)
|
||||
}
|
||||
})
|
||||
bookmarkEntriesToRemove.forEach((itemToRemove) =>{
|
||||
container?.group?.items.remove(itemToRemove)
|
||||
})
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups(container, plugin, bookmarksGroup)
|
||||
})
|
||||
}
|
||||
|
||||
export const _unitTests = {
|
||||
getOrderedBookmarks: getOrderedBookmarks,
|
||||
bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants: bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants,
|
||||
cleanupBookmarkTreeFromTransparentEmptyGroups: cleanupBookmarkTreeFromTransparentEmptyGroups,
|
||||
findGroupForItemPathInBookmarks: findGroupForItemPathInBookmarks
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import {lastPathComponent, extractParentFolderPath} from "./utils";
|
||||
import {
|
||||
lastPathComponent,
|
||||
extractParentFolderPath
|
||||
} from "./utils";
|
||||
|
||||
describe('lastPathComponent and extractParentFolderPath', () => {
|
||||
it.each([
|
||||
|
@ -6,14 +9,14 @@ describe('lastPathComponent and extractParentFolderPath', () => {
|
|||
['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)
|
||||
|
|
|
@ -9,10 +9,10 @@ export function last<T>(o: Array<T>): T | undefined {
|
|||
|
||||
export function lastPathComponent(path: string): string {
|
||||
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
||||
return lastPathSeparatorIdx >= 0 ? path.substring(lastPathSeparatorIdx + 1).trim() : path.trim()
|
||||
return lastPathSeparatorIdx >= 0 ? path.substring(lastPathSeparatorIdx + 1) : path
|
||||
}
|
||||
|
||||
export function extractParentFolderPath(path: string): string {
|
||||
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
||||
return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx).trim() : ''
|
||||
return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx) : ''
|
||||
}
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -763,6 +763,13 @@
|
|||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/codemirror@5.60.8":
|
||||
version "5.60.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.8.tgz#b647d04b470e8e1836dd84b2879988fc55c9de68"
|
||||
integrity sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
||||
|
@ -2396,6 +2403,14 @@ npm-run-path@^4.0.1:
|
|||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
"obsidian-1.4.11@npm:obsidian@1.4.11":
|
||||
version "1.4.11"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.4.11.tgz#5cba594c83a74ebad58b630c610265018abdadaa"
|
||||
integrity sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==
|
||||
dependencies:
|
||||
"@types/codemirror" "5.60.8"
|
||||
moment "2.29.4"
|
||||
|
||||
obsidian@^0.15.4:
|
||||
version "0.15.9"
|
||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2"
|
||||
|
|
Loading…
Reference in New Issue