From 0ba423ce4bf41ebe5b7e4aacc26f3c9c37939a03 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 3 Jan 2023 19:09:37 +0100 Subject: [PATCH] #45 - Feature: explicit matching of 'starred' items - new keyword added to support items starred with Obsidian core plugin 'Starred' - the keyword is `starred:` - detection and more user friendly handling of the general error condition when the File Explorer is not available - new ribbon status icon shape to indicate the general error plus detailed error logged to the console --- README.md | 6 + docs/icons/icon-general-error.png | Bin 0 -> 2630 bytes docs/manual.md | 82 ++++++- docs/syntax-reference.md | 1 + src/custom-sort/custom-sort-types.ts | 3 +- src/custom-sort/custom-sort.spec.ts | 219 +++++++++++++++--- src/custom-sort/custom-sort.ts | 68 +++++- src/custom-sort/icons.ts | 9 + .../sorting-spec-processor.spec.ts | 19 +- src/custom-sort/sorting-spec-processor.ts | 15 +- src/main.ts | 62 ++++- src/types/types.d.ts | 20 +- src/utils/StarredPluginSignature.ts | 11 + 13 files changed, 457 insertions(+), 58 deletions(-) create mode 100644 docs/icons/icon-general-error.png create mode 100644 src/utils/StarredPluginSignature.ts diff --git a/README.md b/README.md index 9ab4287..c802ab6 100644 --- a/README.md +++ b/README.md @@ -578,6 +578,12 @@ States of the ribbon icon: - Fix the problem in specification and click the ribbon icon to re-enable custom sorting. - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in the developer console +- ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. + - File Explorer not available or other type of general error + - File Explorer is a core Obsidian plugin (named __Files__) and thus can be disabled in Obsidian settings + - Some community plugins (like __MAKE.md__) also disable the File Explorer by default + - See obsidinan developer console for detailed error message + - To fix the problem, enable the File Explorer (in Obsidian or in the community plugin responsible for hididing it) - ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. - This can happen when reinstalling the plugin and in similar cases - Click the ribbon icon twice to re-enable the custom sorting. diff --git a/docs/icons/icon-general-error.png b/docs/icons/icon-general-error.png new file mode 100644 index 0000000000000000000000000000000000000000..088eae6c6b0067728a17b41a969b03b2058f5f10 GIT binary patch literal 2630 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 O-x0>b7JbbOfA(+OlQ(Su literal 0 HcmV?d00001 diff --git a/docs/manual.md b/docs/manual.md index f6c87a9..cfb5f14 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1,6 +1,6 @@ > Document is partial, creation in progress -> Please refer to [README.md](../README.md) for usage examples -> Check [syntax-reference.md](./syntax-reference.md), maybe that file has already some content? +> Please refer to [README.md](../README.md) for more usage examples +> Check also [syntax-reference.md](./syntax-reference.md) --- Some sections added ad-hoc, to be integrated later @@ -164,3 +164,81 @@ 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 +## Matching starred items + +The Obsidian core plugin `Starred` allows the user to 'star' files +The keyword `starred:` allows matching such items. A folder is considered _starred_ if at least one immediate child file is starred + +**Example:** + +Consider the below sorting spec: +```yaml +--- +sorting-spec: | + // Example sorting configuration showing + // how to push the starred items to the top + // + // the line below applies the sorting specification + // to all folders in the vault + target-folder: /* + // the sorting order specification for the target folder(s) + > advanced created + // the first group of items captures the files and folders which + // are 'starred' in Obsidian core 'Starred' plugin. + // Items in this group inherit the sorting order of target folder + starred: + // No further groups specified, which means all other items follow the + // starred items, also in the order specified +--- +``` + +The above sorting specification pushes the _starred_ items to the top +To achieve the opposite effect and push the starred items to the bottom, use the below sorting spec: + +```yaml +--- +sorting-spec: | + // Example sorting configuration showing + // how to push the starred items to the bottom + // + // the line below applies the sorting specification + // to all folders in the vault + target-folder: /* + // the sorting order specification for the target folder(s) + > a-z + // the first group of items captures all of the files and folders which don't match any other sorting rule + // Items in this group inherit the sorting order of target folder + /folders:files + // the second group of items captures the files and folders which + // are 'starred' in Obsidian core 'Starred' plugin. + // Items in this group also inherit the sorting order of target folder + starred: +--- +``` + +For a broader view, the same effect (as in previous example) can be achieved using the priorities +of sorting rules: + +```yaml +--- +sorting-spec: | + // Example sorting configuration showing + // how to push the starred items to the bottom + // + // the line below applies the sorting specification + // to all folders in the vault + target-folder: /* + // the sorting order specification for the target folder(s) + > a-z + // the first group of items captures all of the files and folders + // Items in this group inherit the sorting order of target folder + ... + // the second group of items captures the files and folders which + // are 'starred' in Obsidian core 'Starred' plugin. + // Items in this group also inherit the sorting order of target folder + // The priority '/!' indicator tells to evaluate this sorting rule before other rules + // If it were not used, the prevoius rule '...' would eat all of the folders and items + // and the starred items wouldn't be pushed to the bottom + /! starred: +--- +``` diff --git a/docs/syntax-reference.md b/docs/syntax-reference.md index 69c38af..37efa5b 100644 --- a/docs/syntax-reference.md +++ b/docs/syntax-reference.md @@ -144,6 +144,7 @@ Some tokens have shorter equivalents, which can be used interchangeably: - `/:files` --> `/:` e.g. `/:files Chapter \.d+ ...` is equivalent to `/: Chapter \.d+ ...` - `/:files.` --> `/:.` e.g. `/:files. ... \-D+.md` is equivalent to `/:. ... \-D+.md` - `/folders` --> `/` e.g. `/folders Archive...` is equivalent to `/ Archive...` +- `/folders:files` --> `%` e.g. `/folders:files Chapter...` is equivalent to `% Chapter...` Additional shorter equivalents to allow single-liners like `sorting-spec: \< a-z`: - `order-asc:` --> `\<` e.g. `order-asc: modified` is equivalent to `\< modified` diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 04235b2..7eadf36 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -7,7 +7,8 @@ export enum CustomSortGroupType { ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups ExactSuffix, 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 + HasMetadataField, // Notes (or folder's notes) containing a specific metadata field + StarredOnly } export enum CustomSortOrder { diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index a482b30..fabb123 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -11,6 +11,7 @@ import { } 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"; const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { return { @@ -51,7 +52,7 @@ const mockTFolderWithChildren = (name: string): TFolder => { const child4: TFile = mockTFile('Child file 2 created as newest, not modified at all', 'md', 100, TIMESTAMP_NEWEST, TIMESTAMP_NEWEST) const child5: TFile = mockTFile('Child file 3 created inbetween, modified inbetween', 'md', 100, TIMESTAMP_INBETWEEN, TIMESTAMP_INBETWEEN) - return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5]) + return mockTFolder(name, [child1, child2, child3, child4, child5]) } const MockedLoc: Pos = { @@ -879,6 +880,193 @@ describe('determineSortingGroup', () => { } as FolderItemForSorting); }) }) + describe('CustomSortGroupType.StarredOnly', () => { + it('should not match not starred file', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.StarredOnly + }] + } + const starredPluginInstance: Partial = { + findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { + return null + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + starredPluginInstance: starredPluginInstance as Starred_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(1) + }) + it('should match starred file', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.StarredOnly + }] + } + const starredPluginInstance: Partial = { + findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { + return filePath.path === 'Some parent folder/References.md' ? file : null + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + starredPluginInstance: starredPluginInstance as Starred_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(1) + }) + it('should not match empty folder', () => { + // given + const folder: TFolder = mockTFolder('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.StarredOnly + }] + } + const starredPluginInstance: Partial = { + findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { + return filePath.path === 'Some parent folder/References.md' ? {} as TFile : null + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + starredPluginInstance: starredPluginInstance as Starred_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: [], + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(starredPluginInstance.findStarredFile).not.toHaveBeenCalled() + }) + it('should not match folder w/o starred items', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.StarredOnly + }] + } + const starredPluginInstance: Partial = { + findStarredFile: jest.fn( function(filePath: findStarredFile_pathParam): TFile | null { + return filePath.path === 'Some parent folder/References.md' ? {} as TFile : null + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + starredPluginInstance: starredPluginInstance as Starred_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(folder.children.filter(f => (f as any).isRoot === undefined).length) + }) + it('should match folder with one starred item', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.StarredOnly + }] + } + const starredPluginInstance: Partial = { + findStarredFile: jest.fn(function (filePath: findStarredFile_pathParam): TFile | null { + return filePath.path === 'Some parent folder/Child file 2 created as newest, not modified at all.md' ? {} as TFile : null + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + starredPluginInstance: starredPluginInstance as Starred_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + // assume optimized checking of starred items -> first match ends the check + expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(2) + }) + }) describe('when sort by metadata is involved', () => { it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => { // given @@ -1367,34 +1555,10 @@ describe('determineFolderDatesIfNeeded', () => { }], outsidersGroupIdx: OUTSIDERS_GROUP_IDX } - const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items // when const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) - determineFolderDatesIfNeeded([result], sortSpec, cardinality) - - // then - expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME) - expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME) - expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME) - }) - it('should not be triggered if not needed - the folder is an only item', () => { - // given - const folder: TFolder = mockTFolderWithChildren('Test folder 1') - const OUTSIDERS_GROUP_IDX = 0 - const sortSpec: CustomSortSpec = { - targetFoldersPaths: ['/'], - groups: [{ - type: CustomSortGroupType.Outsiders, - order: CustomSortOrder.byModifiedTimeAdvanced - }], - outsidersGroupIdx: OUTSIDERS_GROUP_IDX - } - const cardinality = {[OUTSIDERS_GROUP_IDX]: 1} // Group 0 contains the folder alone - - // when - const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) - determineFolderDatesIfNeeded([result], sortSpec, cardinality) + determineFolderDatesIfNeeded([result], sortSpec) // then expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME) @@ -1413,11 +1577,10 @@ describe('determineFolderDatesIfNeeded', () => { }], outsidersGroupIdx: OUTSIDERS_GROUP_IDX } - const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items // when const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) - determineFolderDatesIfNeeded([result], sortSpec, cardinality) + determineFolderDatesIfNeeded([result], sortSpec) // then expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 9ecf4f1..c09021f 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -1,4 +1,16 @@ -import {FrontMatterCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian'; +import { + App, + FrontMatterCache, + InstalledPlugin, + requireApiVersion, + TAbstractFile, + TFile, + TFolder +} from 'obsidian'; +import { + Starred_PluginInstance, + StarredPlugin_findStarredFile_methodName +} from '../utils/StarredPluginSignature' import { CustomSortGroup, CustomSortGroupType, @@ -140,7 +152,11 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string): return [false, undefined, undefined] } -export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting { +export interface Context { + starredPluginInstance?: Starred_PluginInstance +} + +export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting { let groupIdx: number let determined: boolean = false let matchedGroup: string | null | undefined @@ -235,6 +251,19 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus } } break + case CustomSortGroupType.StarredOnly: + if (ctx?.starredPluginInstance) { + let starred: boolean + if (aFile) { + starred = !!ctx.starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: entry.path}) + } else { // aFolder + starred = determineStarredStatusOfFolder(entry as TFolder, ctx.starredPluginInstance) + } + if (starred) { + determined = true + } + } + break case CustomSortGroupType.MatchAll: determined = true; break @@ -360,11 +389,30 @@ export const determineDatesForFolder = (folder: TFolder, now: number): [Modified return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder] } -export const determineFolderDatesIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec, sortingGroupsCardinality: {[key: number]: number} = {}) => { +export const StarredCorePluginId: string = 'starred' + +export const getStarredPlugin = (app?: App): Starred_PluginInstance | undefined => { + const starredPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(StarredCorePluginId) + if (starredPlugin && starredPlugin.enabled && starredPlugin.instance) { + const starredPluginInstance: Starred_PluginInstance = starredPlugin.instance as Starred_PluginInstance + // defensive programming, in case Obsidian changes its internal APIs + if (typeof starredPluginInstance?.[StarredPlugin_findStarredFile_methodName] === 'function') { + return starredPluginInstance + } + } +} + +export const determineStarredStatusOfFolder = (folder: TFolder, starredPluginInstance: Starred_PluginInstance): boolean => { + return folder.children.some((folderItem) => { + return !isFolder(folderItem) && starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: folderItem.path}) + }) +} + +export const determineFolderDatesIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec) => { const Now: number = Date.now() folderItems.forEach((item) => { const groupIdx: number | undefined = item.groupIdx - if (groupIdx !== undefined && sortingGroupsCardinality[groupIdx] > 1) { + if (groupIdx !== undefined) { const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order if (sortOrderNeedsFolderDates(groupOrder)) { if (item.folder) { @@ -377,8 +425,8 @@ export const determineFolderDatesIfNeeded = (folderItems: Array = (sortingSpec.itemsToHide ? this.file.children.filter((entry: TFile | TFolder) => { @@ -387,16 +435,14 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] : this.file.children) .map((entry: TFile | TFolder) => { - const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec) - const groupIdx: number | undefined = itemForSorting.groupIdx - if (groupIdx !== undefined) { - sortingGroupsCardinality[groupIdx] = 1 + (sortingGroupsCardinality[groupIdx] ?? 0) - } + const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, { + starredPluginInstance: starredPluginInstance + }) return itemForSorting }) // Finally, for advanced sorting by modified date, for some folders the modified date has to be determined - determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality) + determineFolderDatesIfNeeded(folderItems, sortingSpec) folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { return compareTwoItems(itA, itB, sortingSpec); diff --git a/src/custom-sort/icons.ts b/src/custom-sort/icons.ts index 2547c03..1e0a7c0 100644 --- a/src/custom-sort/icons.ts +++ b/src/custom-sort/icons.ts @@ -4,6 +4,7 @@ export const ICON_SORT_ENABLED_ACTIVE: string = 'custom-sort-icon-active' export const ICON_SORT_SUSPENDED: string = 'custom-sort-icon-suspended' export const ICON_SORT_ENABLED_NOT_APPLIED: string = 'custom-sort-icon-enabled-not-applied' export const ICON_SORT_SUSPENDED_SYNTAX_ERROR: string = 'custom-sort-icon-syntax-error' +export const ICON_SORT_SUSPENDED_GENERAL_ERROR: string = 'custom-sort-icon-general-error' export function addIcons() { addIcon(ICON_SORT_ENABLED_ACTIVE, @@ -24,6 +25,14 @@ export function addIcons() { +` + ) + addIcon(ICON_SORT_SUSPENDED_GENERAL_ERROR, + ` + + + + ` ) addIcon(ICON_SORT_ENABLED_NOT_APPLIED, diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 88726da..482acaa 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -29,6 +29,9 @@ target-folder: tricky folder < a-z by-metadata: Some-dedicated-field with-metadata: Pages > a-z by-metadata: +starred: +/:files starred: +/folders starred: :::: Conceptual model /: Entities @@ -82,6 +85,9 @@ target-folder: tricky folder 2 < a-z by-metadata: Some-dedicated-field % with-metadata: Pages > a-z by-metadata: +/folders:files starred: +/:files starred: +/folders starred: :::: Conceptual model /:files Entities @@ -165,11 +171,22 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: 'Pages', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse + }, { + type: CustomSortGroupType.StarredOnly, + order: CustomSortOrder.alphabetical + }, { + type: CustomSortGroupType.StarredOnly, + filesOnly: true, + order: CustomSortOrder.alphabetical + }, { + type: CustomSortGroupType.StarredOnly, + foldersOnly: true, + order: CustomSortOrder.alphabetical }, { order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }], - outsidersGroupIdx: 2, + outsidersGroupIdx: 5, targetFoldersPaths: [ 'tricky folder 2' ] diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index da239e8..c695d72 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -188,12 +188,15 @@ const FilesWithExtGroupShortLexeme: string = '/:.' const FoldersGroupVerboseLexeme: string = '/folders' const FoldersGroupShortLexeme: string = '/' const AnyTypeGroupLexemeShort: string = '%' // See % as a combination of / and : -const AnyTypeGroupLexeme: string = '/%' // See % as a combination of / and : +const AnyTypeGroupLexeme1: string = '/folders:files' +const AnyTypeGroupLexeme2: string = '/%' // See % as a combination of / and : const HideItemShortLexeme: string = '--%' // See % as a combination of / and : const HideItemVerboseLexeme: string = '/--hide:' const MetadataFieldIndicatorLexeme: string = 'with-metadata:' +const StarredItemsIndicatorLexeme: string = 'starred:' + const CommentPrefix: string = '//' const PriorityModifierPrio1Lexeme: string = '/!' @@ -232,7 +235,8 @@ const SortingGroupPrefixes: { [key: string]: SortingGroupType } = { [FoldersGroupShortLexeme]: {foldersOnly: true}, [FoldersGroupVerboseLexeme]: {foldersOnly: true}, [AnyTypeGroupLexemeShort]: {}, - [AnyTypeGroupLexeme]: {}, + [AnyTypeGroupLexeme1]: {}, + [AnyTypeGroupLexeme2]: {}, [HideItemShortLexeme]: {itemToHide: true}, [HideItemVerboseLexeme]: {itemToHide: true} } @@ -1337,6 +1341,13 @@ export class SortingSpecProcessor { foldersOnly: spec.foldersOnly, matchFilenameWithExt: spec.matchFilenameWithExt } + } else if (theOnly.startsWith(StarredItemsIndicatorLexeme)) { + return { + type: CustomSortGroupType.StarredOnly, + filesOnly: spec.filesOnly, + foldersOnly: spec.foldersOnly, + matchFilenameWithExt: spec.matchFilenameWithExt + } } else { // For non-three dots single text line assume exact match group return { diff --git a/src/main.ts b/src/main.ts index 2022d3f..d6d406d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,6 +23,7 @@ import { ICON_SORT_ENABLED_ACTIVE, ICON_SORT_ENABLED_NOT_APPLIED, ICON_SORT_SUSPENDED, + ICON_SORT_SUSPENDED_GENERAL_ERROR, ICON_SORT_SUSPENDED_SYNTAX_ERROR } from "./custom-sort/icons"; @@ -124,8 +125,39 @@ export default class CustomSortPlugin extends Plugin { } } + checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined { + let fileExplorerView: FileExplorerView | undefined = this.getFileExplorer() + if (fileExplorerView + && typeof fileExplorerView.createFolderDom === 'function' + && typeof fileExplorerView.requestSort === 'function') { + return fileExplorerView + } else { + // Various scenarios when File Explorer was turned off (e.g. by some other plugin) + if (logWarning) { + this.logWarningFileExplorerNotAvailable() + } + return undefined + } + } + + logWarningFileExplorerNotAvailable() { + const msg = `custom-sort v${this.manifest.version}: failed to locate File Explorer. The 'Files' core plugin can be disabled.\n` + + `Some community plugins can also disable it.\n` + + `See the example of MAKE.md plugin: https://github.com/Make-md/makemd/issues/25\n` + + `You can find there instructions on how to re-enable the File Explorer in MAKE.md plugin` + console.warn(msg) + } + // Safe to suspend when suspended and re-enable when enabled switchPluginStateTo(enabled: boolean, updateRibbonBtnIcon: boolean = true) { + let fileExplorerView: FileExplorerView | undefined = this.checkFileExplorerIsAvailableAndPatchable() + if (fileExplorerView && !this.fileExplorerFolderPatched) { + this.fileExplorerFolderPatched = this.patchFileExplorerFolder(fileExplorerView); + + if (!this.fileExplorerFolderPatched) { + fileExplorerView = undefined + } + } this.settings.suspended = !enabled; this.saveSettings() let iconToSet: string @@ -136,20 +168,24 @@ export default class CustomSortPlugin extends Plugin { } else { this.readAndParseSortingSpec(); if (this.sortSpecCache) { - this.showNotice('Custom sort ON'); - this.initialAutoOrManualSortingTriggered = true - iconToSet = ICON_SORT_ENABLED_ACTIVE + if (fileExplorerView) { + this.showNotice('Custom sort ON'); + this.initialAutoOrManualSortingTriggered = true + iconToSet = ICON_SORT_ENABLED_ACTIVE + } else { + this.showNotice('Custom sort GENERAL PROBLEM. See console for detailed message.'); + iconToSet = ICON_SORT_SUSPENDED_GENERAL_ERROR + this.settings.suspended = true + this.saveSettings() + } } else { iconToSet = ICON_SORT_SUSPENDED_SYNTAX_ERROR this.settings.suspended = true this.saveSettings() } } - const fileExplorerView: FileExplorerView | undefined = this.getFileExplorer() + if (fileExplorerView) { - if (!this.fileExplorerFolderPatched) { - this.fileExplorerFolderPatched = this.patchFileExplorerFolder(fileExplorerView); - } if (this.fileExplorerFolderPatched) { fileExplorerView.requestSort(); } @@ -215,7 +251,7 @@ export default class CustomSortPlugin extends Plugin { this.initialAutoOrManualSortingTriggered = true if (this.sortSpecCache) { // successful read of sorting specifications? this.showNotice('Custom sort ON') - const fileExplorerView: FileExplorerView | undefined = this.getFileExplorer() + const fileExplorerView: FileExplorerView | undefined = this.checkFileExplorerIsAvailableAndPatchable(false) if (fileExplorerView) { setIcon(this.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE) fileExplorerView.requestSort() @@ -260,13 +296,15 @@ export default class CustomSortPlugin extends Plugin { } // For the idea of monkey-patching credits go to https://github.com/nothingislost/obsidian-bartender - patchFileExplorerFolder(fileExplorer?: FileExplorerView): boolean { + patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean { let plugin = this; - fileExplorer = fileExplorer ?? this.getFileExplorer() - if (fileExplorer) { + // patching file explorer might fail here because of various non-error reasons. + // That's why not showing and not logging error message here + patchableFileExplorer = patchableFileExplorer ?? this.checkFileExplorerIsAvailableAndPatchable(false) + if (patchableFileExplorer) { // @ts-ignore let tmpFolder = new TFolder(Vault, ""); - let Folder = fileExplorer.createFolderDom(tmpFolder).constructor; + let Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor; const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, { sort(old: any) { return function (...args: any[]) { diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 58e02fa..240f256 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -1,4 +1,4 @@ -import {TFolder, WorkspaceLeaf} from "obsidian"; +import {PluginInstance, TFolder, WorkspaceLeaf} from "obsidian"; // Needed to support monkey-patching of the folder sort() function @@ -7,10 +7,28 @@ declare module 'obsidian' { viewByType: Record unknown>; } + // undocumented internal interface - for experimental features + export interface PluginInstance { + id: string; + } + export interface App { + internalPlugins: InternalPlugins; // undocumented internal API - for experimental features viewRegistry: ViewRegistry; } + // undocumented internal interface - for experimental features + export interface InstalledPlugin { + enabled: boolean; + instance: PluginInstance; + } + + // undocumented internal interface - for experimental features + export interface InternalPlugins { + plugins: Record; + getPluginById(id: string): InstalledPlugin; + } + interface FileExplorerFolder { } diff --git a/src/utils/StarredPluginSignature.ts b/src/utils/StarredPluginSignature.ts new file mode 100644 index 0000000..db5df02 --- /dev/null +++ b/src/utils/StarredPluginSignature.ts @@ -0,0 +1,11 @@ +import {PluginInstance, TFile} from "obsidian"; + +export const StarredPlugin_findStarredFile_methodName = 'findStarredFile' + +export interface findStarredFile_pathParam { + path: string +} + +export interface Starred_PluginInstance extends PluginInstance { + [StarredPlugin_findStarredFile_methodName]: (filePath: findStarredFile_pathParam) => TFile | null +}