diff --git a/docs/manual.md b/docs/manual.md index 8e0a4fb..a712be5 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -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\ diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index efbeb01..21c232f 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -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 @@ -30,7 +28,7 @@ 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 @@ -80,10 +78,7 @@ export interface CustomSortSpec { outsidersFoldersGroupIdx?: number itemsToHide?: Set priorityOrder?: Array // Indexes of groups in evaluation order - - // For internal transient use - plugin?: Plugin // to hand over the access to App instance to the sorting engine - _mCache?: MetadataCache + implicit?: boolean // spec applied automatically (e.g. auto integration with a plugin) } export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value' diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index b558fc7..dd1e398 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -7,7 +7,7 @@ import { FolderItemForSorting, matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField, SorterFn, - Sorters + getSorterFnFor, ProcessingContext } from './custom-sort'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; @@ -716,7 +716,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -732,7 +734,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -753,7 +755,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -769,7 +773,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -790,7 +794,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -806,7 +812,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -827,7 +833,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -843,7 +851,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(folder, sortSpec) + const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -876,7 +884,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -907,7 +915,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -938,7 +946,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -977,7 +985,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1016,7 +1024,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1058,7 +1066,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1093,7 +1101,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1127,7 +1135,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1162,7 +1170,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1193,7 +1201,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1235,7 +1243,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1280,7 +1288,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1323,7 +1331,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1369,7 +1377,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1412,7 +1420,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1458,7 +1466,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1491,7 +1499,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1507,7 +1517,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1530,7 +1540,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1546,7 +1558,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1569,7 +1581,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldTrueAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1585,7 +1599,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1608,7 +1622,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1624,7 +1640,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1647,7 +1663,9 @@ describe('determineSortingGroup', () => { exactPrefix: 'Ref', byMetadataField: 'metadata-field-for-sorting', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1663,7 +1681,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(folder, sortSpec) + const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1687,6 +1705,10 @@ describe('determineSortingGroup', () => { exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical }], + defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder' + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1698,13 +1720,11 @@ describe('determineSortingGroup', () => { } }[path] } - } as MetadataCache, - defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, - byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder', + } as MetadataCache } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1726,7 +1746,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, order: CustomSortOrder.byMetadataFieldAlphabetical, withMetadataFieldName: 'field-used-with-with-metadata-syntax' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1742,7 +1764,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1764,7 +1786,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.ExactPrefix, exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1780,7 +1804,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -2094,7 +2118,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { metadataFieldValue: 'B' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2114,7 +2138,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { metadataFieldValue: 'Aaa', sortString: 'a123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2135,7 +2159,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { sortString: 'n123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2153,7 +2177,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { sortString: 'ccc ' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2176,7 +2200,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { metadataFieldValue: 'B' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2196,7 +2220,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { metadataFieldValue: 'Aaa', sortString: 'a123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2217,7 +2241,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { sortString: 'n123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2236,7 +2260,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { sortString: 'n123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2254,7 +2278,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { sortString: 'ccc ' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 7ce7a87..36b4d57 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -1,8 +1,7 @@ -import {FrontMatterCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian'; -import {determineStarredStatusOf, getStarredPlugin, Starred_PluginInstance} from '../utils/StarredPluginSignature'; +import {FrontMatterCache, MetadataCache, Plugin, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian'; +import {determineStarredStatusOf, Starred_PluginInstance} from '../utils/StarredPluginSignature'; import { determineIconOf, - getIconFolderPlugin, ObsidianIconFolder_PluginInstance } from '../utils/ObsidianIconFolderPluginSignature' import { @@ -17,10 +16,21 @@ import { import {isDefined} from "../utils/utils"; import { Bookmarks_PluginInstance, - determineBookmarkOrder, - getBookmarksPlugin + determineBookmarkOrder } 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 + bookmarksPlugin: { + instance?: Bookmarks_PluginInstance, + groupNameForSorting?: string + } + iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance +} + let CollatorCompare = new Intl.Collator(undefined, { usage: "sort", sensitivity: "base", @@ -95,7 +105,7 @@ export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: b } } -export let Sorters: { [key in CustomSortOrder]: SorterFn } = { +let Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), [CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString), [CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString), @@ -115,29 +125,62 @@ export let Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder), [CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder), - // This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all + // 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), }; -function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) { - if (itA.groupIdx != undefined && itB.groupIdx != undefined) { - if (itA.groupIdx === itB.groupIdx) { - const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx] - const matchingGroupPresentOnBothSidesAndEqual: boolean = itA.matchGroup !== undefined && itA.matchGroup === itB.matchGroup - if (matchingGroupPresentOnBothSidesAndEqual && group.secondaryOrder) { - return Sorters[group.secondaryOrder ?? CustomSortOrder.default](itA, itB) +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)' +} + +// Standard Obsidian comparator keeps folders in the top sorted alphabetically +const StandardObsidianComparator = (order: CustomSortOrder): SorterFn => { + const customSorterFn = Sorters[order] + return (a: FolderItemForSorting, b: FolderItemForSorting): number => { + return a.isFolder || b.isFolder + ? + (a.isFolder && !b.isFolder ? -1 : (b.isFolder && !a.isFolder ? 1 : Sorters[CustomSortOrder.alphabetical](a,b))) + : + customSorterFn(a, b); + } +} + +export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => { + if (sorting === CustomSortOrder.standardObsidian) { + sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical + return StandardObsidianComparator(sorting) + } else { + return Sorters[sorting] + } +} + +function getComparator(sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn { + const compareTwoItems = (itA: FolderItemForSorting, itB: FolderItemForSorting) => { + if (itA.groupIdx != undefined && itB.groupIdx != undefined) { + if (itA.groupIdx === itB.groupIdx) { + const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx] + const matchingGroupPresentOnBothSidesAndEqual: boolean = itA.matchGroup !== undefined && itA.matchGroup === itB.matchGroup + if (matchingGroupPresentOnBothSidesAndEqual && group.secondaryOrder) { + return getSorterFnFor(group.secondaryOrder ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB) + } else { + return getSorterFnFor(group?.order ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB) + } } else { - return Sorters[group?.order ?? CustomSortOrder.default](itA, itB) + return itA.groupIdx - itB.groupIdx; } } else { - return itA.groupIdx - itB.groupIdx; + // should never happen - groupIdx is not known for at least one of items to compare. + // The logic of determining the index always sets some idx + // Yet for sanity and to satisfy TS code analyzer a fallback to default behavior below + return getSorterFnFor(CustomSortOrder.default, currentUIselectedSorting)(itA, itB) } - } else { - // should never happen - groupIdx is not known for at least one of items to compare. - // The logic of determining the index always sets some idx - // Yet for sanity and to satisfy TS code analyzer a fallback to default behavior below - return Sorters[CustomSortOrder.default](itA, itB) } + return compareTwoItems } const isFolder = (entry: TAbstractFile) => { @@ -171,13 +214,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string): return [false, undefined, undefined] } -export interface Context { - starredPluginInstance?: Starred_PluginInstance - bookmarksPluginInstance?: Bookmarks_PluginInstance - iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance -} - -export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting { +export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: ProcessingContext): FolderItemForSorting { let groupIdx: number let determined: boolean = false let matchedGroup: string | null | undefined @@ -261,10 +298,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus break case CustomSortGroupType.HasMetadataField: if (group.withMetadataFieldName) { - if (spec._mCache) { + if (ctx?._mCache) { // For folders - scan metadata of 'folder note' const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` - const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter + const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName) if (hasMetadata) { @@ -282,8 +319,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus } break case CustomSortGroupType.BookmarkedOnly: - if (ctx?.bookmarksPluginInstance) { - const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPluginInstance) + if (ctx?.bookmarksPlugin?.instance) { + const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPlugin?.instance, ctx.bookmarksPlugin?.groupNameForSorting) if (bookmarkOrder) { // safe ==> orders intentionally start from 1 determined = true bookmarkedIdx = bookmarkOrder @@ -362,10 +399,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING } if (metadataFieldName) { - if (spec._mCache) { + if (ctx?._mCache) { // For folders - scan metadata of 'folder note' const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` - const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter + const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter metadataValueToSortBy = frontMatterCache?.[metadataFieldName] } } @@ -455,7 +492,7 @@ export const determineFolderDatesIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance) => { +export const determineBookmarksOrderIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => { if (!plugin) return folderItems.forEach((item) => { @@ -469,17 +506,13 @@ export const determineBookmarksOrderIfNeeded = (folderItems: Array = (sortingSpec.itemsToHide ? this.file.children.filter((entry: TFile | TFolder) => { @@ -488,24 +521,20 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] : this.file.children) .map((entry: TFile | TFolder) => { - const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, { - starredPluginInstance: starredPluginInstance, - bookmarksPluginInstance: bookmarksPluginInstance, - iconFolderPluginInstance: iconFolderPluginInstance - }) + 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 (bookmarksPluginInstance) { - determineBookmarksOrderIfNeeded(folderItems, sortingSpec, bookmarksPluginInstance) + if (ctx.bookmarksPlugin?.instance) { + determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPlugin.instance, ctx.bookmarksPlugin.groupNameForSorting) } - folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { - return compareTwoItems(itA, itB, sortingSpec); - }); + const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder) + + folderItems.sort(comparator) const items = folderItems .map((item: FolderItemForSorting) => fileExplorer.fileItems[item.path]) @@ -515,8 +544,4 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] } else { this.children = items; } - - // release risky references - sortingSpec._mCache = undefined - sortingSpec.plugin = undefined }; diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index 8cb8af9..d52db4c 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -2,8 +2,10 @@ import {FolderWildcardMatching} from './folder-matching-rules' type SortingSpec = string +const checkIfImplicitSpec = (s: SortingSpec) => false + const createMockMatcherRichVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) let p: string p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`) p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`) @@ -19,25 +21,25 @@ const PRIO2 = 2 const PRIO3 = 3 const createMockMatcherSimplestVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') return matcher } const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/...', '/...') return matcher } const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/*', '/*') return matcher } const createMockMatcherSimpleVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...') return matcher @@ -108,21 +110,21 @@ describe('folderMatch', () => { expect(match3).toBe('/Reviews/daily/...') }) it('should detect duplicate match children definitions for same path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence') const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate') expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"}) }) it('should detect duplicate match all definitions for same path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence') const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate') expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"}) }) it('regexp-match by name works (order of regexp doesn\'t matter) case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -134,7 +136,7 @@ describe('folderMatch', () => { expect(match2).toBe('r2') }) it('regexp-match by name works (order of regexp doesn\'t matter) reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -146,7 +148,7 @@ describe('folderMatch', () => { expect(match2).toBe('r2') }) it('regexp-match by path works (order of regexp doesn\'t matter) case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -158,7 +160,7 @@ describe('folderMatch', () => { expect(match2).toBe('r1') }) it('regexp-match by path works (order of regexp doesn\'t matter) reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -170,7 +172,7 @@ describe('folderMatch', () => { expect(match2).toBe('r1') }) it('regexp-match by path and name for root level - order of regexp decides - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -179,7 +181,7 @@ describe('folderMatch', () => { expect(match).toBe('r2') }) it('regexp-match by path and name for root level - order of regexp decides - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -188,7 +190,7 @@ describe('folderMatch', () => { expect(match).toBe('r1') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) @@ -198,7 +200,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) @@ -208,7 +210,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match priorities - order of definitions irrelevant - duplicate priorities - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3a`) matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`) matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`) @@ -221,7 +223,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3b') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) @@ -231,14 +233,14 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match - edge case of matching the root folder - match by path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^\/$/, false, undefined, false, `r1`) // Path w/o leading / - this is how Obsidian supplies the path const match: SortingSpec | null = matcher.folderMatch('/', '') expect(match).toBe('r1') }) it('regexp-match - edge case of matching the root folder - match by name not possible', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) // Tricky regexp which can return zero length matches matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`) matcher.addWildcardDefinition('/*', `w1`) @@ -247,7 +249,7 @@ describe('folderMatch', () => { expect(match).toBe('w1') }) it('regexp-match - edge case of no match when only regexp rules present', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) // Tricky regexp which can return zero length matches matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`) // Path w/o leading / - this is how Obsidian supplies the path diff --git a/src/custom-sort/folder-matching-rules.ts b/src/custom-sort/folder-matching-rules.ts index bd18721..55f558b 100644 --- a/src/custom-sort/folder-matching-rules.ts +++ b/src/custom-sort/folder-matching-rules.ts @@ -35,6 +35,8 @@ export interface AddingWildcardFailure { errorMsg: string } +export type CheckIfImplicitSpec = (s: SortingSpec) => boolean + export class FolderWildcardMatching { // mimics the structure of folders, so for example tree.matchAll contains the matchAll flag for the root '/' @@ -44,6 +46,9 @@ export class FolderWildcardMatching { regexps: Array> + constructor(private checkIfImplicitSpec: CheckIfImplicitSpec) { + } + // cache determinedWildcardRules: { [key: string]: DeterminedSortingSpec } = {} @@ -68,13 +73,13 @@ export class FolderWildcardMatching { } }) if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) { - if (leafNode.matchChildren) { + if (leafNode.matchChildren && !this.checkIfImplicitSpec(leafNode.matchChildren)) { return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`} } else { leafNode.matchChildren = rule } } else { // Implicitly: MATCH_ALL_PATH_TOKEN - if (leafNode.matchAll) { + if (leafNode.matchAll && !this.checkIfImplicitSpec(leafNode.matchAll)) { return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`} } else { leafNode.matchAll = rule diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 7d27669..b48aba2 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -527,16 +527,23 @@ describe('SortingSpecProcessor', () => { const txtInputStandardObsidianSortAttr: string = ` target-folder: AAA sorting: standard +/ Some folder + sorting: standard ` const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = { "AAA": { defaultOrder: CustomSortOrder.standardObsidian, groups: [{ + exactText: 'Some folder', + foldersOnly: true, + order: CustomSortOrder.standardObsidian, + type: CustomSortGroupType.ExactName + }, { order: CustomSortOrder.standardObsidian, type: CustomSortGroupType.Outsiders }], - outsidersGroupIdx: 0, + outsidersGroupIdx: 1, targetFoldersPaths: ['AAA'] } } @@ -1687,11 +1694,13 @@ const txtInputErrorTooManyNumericSortSymbols: string = ` % Chapter\\R+ ... page\\d+ ` +/* No longer applicable const txtInputErrorNestedStandardObsidianSortAttr: string = ` target-folder: AAA / Some folder sorting: standard ` +*/ const txtInputErrorPriorityEmptyFilePattern: string = ` /!! /: @@ -1797,6 +1806,7 @@ describe('SortingSpecProcessor error detection and reporting', () => { `${ERR_PREFIX} 9:TooManySortingSymbols Maximum one sorting symbol allowed per line ${ERR_SUFFIX_IN_LINE(2)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ ')) }) + /* Problem no longer applicable it('should recognize error: nested standard obsidian sorting attribute', () => { const inputTxtArr: Array = txtInputErrorNestedStandardObsidianSortAttr.split('\n') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') @@ -1806,6 +1816,7 @@ describe('SortingSpecProcessor error detection and reporting', () => { `${ERR_PREFIX} 14:StandardObsidianSortAllowedOnlyAtFolderLevel The standard Obsidian sort order is only allowed at a folder level (not nested syntax) ${ERR_SUFFIX_IN_LINE(4)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard')) }) + */ it('should recognize error: priority indicator alone', () => { const inputTxtArr: Array = ` /! diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index d323b43..ecff8a0 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -37,6 +37,7 @@ interface ProcessingContext { specs: Array currentSpec?: CustomSortSpec currentSpecGroup?: CustomSortGroup + implicitSpec?: boolean // Support for specific conditions (intentionally not generic approach) previousValidEntryWasTargetFolderAttr?: boolean // Entry in previous non-empty valid line @@ -69,7 +70,7 @@ export enum ProblemCode { ItemToHideExactNameWithExtRequired, ItemToHideNoSupportForThreeDots, DuplicateWildcardSortSpecForSameFolder, - StandardObsidianSortAllowedOnlyAtFolderLevel, + ProblemNoLongerApplicable_StandardObsidianSortAllowedOnlyAtFolderLevel, // Placeholder kept to avoid refactoring of many unit tests (hardcoded error codes) PriorityNotAllowedOnOutsidersGroup, TooManyPriorityPrefixes, CombiningNotAllowedOnOutsidersGroup, @@ -580,7 +581,7 @@ const ensureCollectionHasSortSpecByName = (collection?: SortSpecsCollection | nu const ensureCollectionHasSortSpecByWildcard = (collection?: SortSpecsCollection | null) => { collection = collection ?? {} if (!collection.sortSpecByWildcard) { - collection.sortSpecByWildcard = new FolderWildcardMatching() + collection.sortSpecByWildcard = new FolderWildcardMatching((spec: CustomSortSpec) => !!spec.implicit) } return collection } @@ -605,35 +606,38 @@ const endsWithWildcardPatternSuffix = (path: string): boolean => { enum WildcardPriority { NO_WILDCARD = 1, + NO_WILDCARD_IMPLICIT, MATCH_CHILDREN, - MATCH_ALL + MATCH_CHILDREN_IMPLICIT, + MATCH_ALL, + MATCH_ALL_IMPLICIT } -const stripWildcardPatternSuffix = (path: string): {path: string, detectedWildcardPriority: number} => { +const stripWildcardPatternSuffix = (path: string, ofImplicitSpec: boolean): {path: string, detectedWildcardPriority: number} => { if (path.endsWith(MATCH_ALL_SUFFIX)) { path = path.slice(0, -MATCH_ALL_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_ALL + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_ALL_IMPLICIT : WildcardPriority.MATCH_ALL } } if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN, + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN } } if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN } } return { path: path, - detectedWildcardPriority: WildcardPriority.NO_WILDCARD + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.NO_WILDCARD_IMPLICIT : WildcardPriority.NO_WILDCARD } } @@ -730,12 +734,14 @@ export class SortingSpecProcessor { parseSortSpecFromText(text: Array, folderPath: string, sortingSpecFileName: string, - collection?: SortSpecsCollection | null + collection?: SortSpecsCollection | null, + implicitSpec?: boolean ): SortSpecsCollection | null | undefined { // reset / init processing state after potential previous invocation this.ctx = { folderPath: folderPath, // location of the sorting spec file - specs: [] + specs: [], + implicitSpec: implicitSpec }; this.currentEntryLine = null this.currentEntryLineIdx = null @@ -842,7 +848,7 @@ export class SortingSpecProcessor { for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) { const originalPath = spec.targetFoldersPaths[idx] if (!originalPath.startsWith(MatchFolderNameLexeme) && !originalPath.startsWith(MatchFolderByRegexpLexeme)) { - const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath) + const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath, !!spec.implicit) let storeTheSpec: boolean = true const preexistingSortSpecPriority: WildcardPriority = this.pathMatchPriorityForPath[path] if (preexistingSortSpecPriority) { @@ -974,10 +980,6 @@ export class SortingSpecProcessor { this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`) return false; } - if ((attr.value as RecognizedOrderValue).order === CustomSortOrder.standardObsidian) { - this.problem(ProblemCode.StandardObsidianSortAllowedOnlyAtFolderLevel, `The standard Obsidian sort order is only allowed at a folder level (not nested syntax)`) - return false; - } this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder @@ -1453,7 +1455,8 @@ export class SortingSpecProcessor { private putNewSpecForNewTargetFolder(folderPath?: string): CustomSortSpec { const newSpec: CustomSortSpec = { targetFoldersPaths: [folderPath ?? this.ctx.folderPath], - groups: [] + groups: [], + implicit: this.ctx.implicitSpec } this.ctx.specs.push(newSpec); diff --git a/src/main.ts b/src/main.ts index 2118215..f7e4c4a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { App, - FileExplorerView, + FileExplorerView, Menu, MenuItem, MetadataCache, normalizePath, Notice, @@ -13,10 +13,10 @@ import { TAbstractFile, TFile, TFolder, - Vault + Vault, WorkspaceLeaf } from 'obsidian'; import {around} from 'monkey-around'; -import {folderSort} from './custom-sort/custom-sort'; +import {folderSort, ProcessingContext} from './custom-sort/custom-sort'; import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor'; import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types'; @@ -29,6 +29,9 @@ import { ICON_SORT_SUSPENDED_GENERAL_ERROR, ICON_SORT_SUSPENDED_SYNTAX_ERROR } from "./custom-sort/icons"; +import {getStarredPlugin} from "./utils/StarredPluginSignature"; +import {getBookmarksPlugin} from "./utils/BookmarksCorePluginSignature"; +import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature"; interface CustomSortPluginSettings { additionalSortspecFile: string @@ -36,7 +39,8 @@ interface CustomSortPluginSettings { statusBarEntryEnabled: boolean notificationsEnabled: boolean mobileNotificationsEnabled: boolean - enableAutomaticBookmarksOrderIntegration: boolean + automaticBookmarksIntegration: boolean + bookmarksGroupToConsumeAsOrderingReference: string } const DEFAULT_SETTINGS: CustomSortPluginSettings = { @@ -45,7 +49,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = { statusBarEntryEnabled: true, notificationsEnabled: true, mobileNotificationsEnabled: false, - enableAutomaticBookmarksOrderIntegration: false + automaticBookmarksIntegration: false, + bookmarksGroupToConsumeAsOrderingReference: 'sortspec' } const SORTSPEC_FILE_NAME: string = 'sortspec.md' @@ -53,6 +58,13 @@ const SORTINGSPEC_YAML_KEY: string = 'sorting-spec' const ERROR_NOTICE_TIMEOUT: number = 10000 +const ImplicitSortspecForBookmarksIntegration: string = ` +target-folder: /* +bookmarked: + < by-bookmarks-order +sorting: standard +` + // the monkey-around package doesn't export the below type type MonkeyAroundUninstaller = () => void @@ -82,12 +94,13 @@ export default class CustomSortPlugin extends Plugin { this.sortSpecCache = null const processor: SortingSpecProcessor = new SortingSpecProcessor() - if (this.settings.enableAutomaticBookmarksOrderIntegration) { + if (this.settings.automaticBookmarksIntegration) { this.sortSpecCache = processor.parseSortSpecFromText( - 'target-folder: /*\n< by-bookmarks-order'.split('\n'), + 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 + this.sortSpecCache, + true // Implicit sorting spec generation ) console.log('Auto injected sort spec') console.log(this.sortSpecCache) @@ -296,6 +309,29 @@ export default class CustomSortPlugin extends Plugin { } }) ); + + this.registerEvent( + this.app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => { + const bookmarkThisMenuItem = (item: MenuItem) => { + // TODO: if already bookmarked in the 'custom sort' group (or its descendants) don't show + item.setTitle('Custom sort: bookmark for sorting.'); + item.setIcon('hashtag'); + item.onClick(() => { + console.log(`custom-sort: bookmark this clicked ${source}`) + }); + }; + const bookmarkAllMenuItem = (item: MenuItem) => { + item.setTitle('Custom sort: bookmark all siblings for sorting.'); + item.setIcon('hashtag'); + item.onClick(() => { + console.log(`custom-sort: bookmark all siblings clicked ${source}`) + }); + }; + + menu.addItem(bookmarkThisMenuItem) + menu.addItem(bookmarkAllMenuItem) + }) + ) } registerCommands() { @@ -335,6 +371,10 @@ 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); @@ -349,21 +389,34 @@ export default class CustomSortPlugin extends Plugin { const folder: TFolder = this.file let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path] sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name] - if (sortSpec) { - if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) { - sortSpec = null // A folder is explicitly excluded from custom sorting plugin - } - } else if (plugin.sortSpecCache?.sortSpecByWildcard) { + + 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) - if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) { + /* 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 + if (sortSpec) { - sortSpec.plugin = plugin - return folderSort.call(this, sortSpec, ...args); + 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); } else { + console.log(`NO Sortspec for folder ${folder.path}`) return old.call(this, ...args); } }; @@ -490,17 +543,67 @@ class CustomSortSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + 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.' + + '
' + + '

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

' + + '

More information on this functionality in the ' + + '' + + 'manual of this custom-sort plugin.' + + '

' + ) + new Setting(containerEl) - .setName('Enable automatic integration with core Bookmarks plugin') + .setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)') // TODO: add a nice description here - .setDesc('Details TBD. TODO: add a nice description here') + .setDesc(bookmarksIntegrationDescription) .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableAutomaticBookmarksOrderIntegration) + .setValue(this.plugin.settings.automaticBookmarksIntegration) .onChange(async (value) => { - this.plugin.settings.enableAutomaticBookmarksOrderIntegration = value; + this.plugin.settings.automaticBookmarksIntegration = value; await this.plugin.saveSettings(); })); - // TODO: expose additional configuration setting to specify group path in Bookmarks, if auto-integration with bookmarks is enabled + 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) => { + this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim(); + 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: 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: fix error +// bookmarks integration - for root folder and for other folders +// (check for the case: +// target-folder: /* +// sorting: standard + +// 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 diff --git a/src/utils/BookmarksCorePluginSignature.ts b/src/utils/BookmarksCorePluginSignature.ts index e0cd5e0..8ca6b10 100644 --- a/src/utils/BookmarksCorePluginSignature.ts +++ b/src/utils/BookmarksCorePluginSignature.ts @@ -86,27 +86,29 @@ export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefi } } -type TraverseCallback = (item: BookmarkedItem) => boolean | void +type TraverseCallback = (item: BookmarkedItem, groupPath: string) => boolean | void const traverseBookmarksCollection = (items: Array, callback: TraverseCallback) => { - const recursiveTraversal = (collection: Array) => { + const recursiveTraversal = (collection: Array, groupPath: string) => { for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) { const item = collectionRef[idx]; - if (callback(item)) return; - if ('group' === item.type) recursiveTraversal(item.items); + if (callback(item, groupPath)) return; + if ('group' === item.type) recursiveTraversal(item.items, `${groupPath}${groupPath ? '/' : ''}${item.title}`); } }; - recursiveTraversal(items); + recursiveTraversal(items, ''); } -// TODO: extend this function to take a scope as parameter: a path to Bookmarks group to start from -// Initially consuming all bookmarks is ok - finally the starting point (group) should be configurable -const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks | undefined => { +const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): OrderedBookmarks | undefined => { const bookmarks: Array | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]() if (bookmarks) { const orderedBookmarks: OrderedBookmarks = {} let order: number = 0 - const consumeItem = (item: BookmarkedItem) => { + const groupNamePrefix: string = bookmarksGroup ? `${bookmarksGroup}/` : '' + const consumeItem = (item: BookmarkedItem, groupPath: string) => { + if (groupNamePrefix && !groupPath.startsWith(groupNamePrefix)) { + return + } const isFile: boolean = item.type === 'file' const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath const isFolder: boolean = item.type === 'folder' @@ -133,9 +135,9 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks // undefined ==> item not found in bookmarks // > 0 ==> item found in bookmarks at returned position // Intentionally not returning 0 to allow simple syntax of processing the result -export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance): number | undefined => { +export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): number | undefined => { if (!bookmarksCache) { - bookmarksCache = getOrderedBookmarks(plugin) + bookmarksCache = getOrderedBookmarks(plugin, bookmarksGroup) bookmarksCacheTimestamp = Date.now() }