#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
This commit is contained in:
SebastianMC 2023-01-03 19:09:37 +01:00
parent 9fbc98c163
commit 0ba423ce4b
13 changed files with 457 additions and 58 deletions

View File

@ -578,6 +578,12 @@ States of the ribbon icon:
- Fix the problem in specification and click the ribbon icon to re-enable custom sorting. - 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 - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in
the developer console 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. - ![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 - This can happen when reinstalling the plugin and in similar cases
- Click the ribbon icon twice to re-enable the custom sorting. - Click the ribbon icon twice to re-enable the custom sorting.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,6 +1,6 @@
> Document is partial, creation in progress > Document is partial, creation in progress
> Please refer to [README.md](../README.md) for usage examples > Please refer to [README.md](../README.md) for more usage examples
> Check [syntax-reference.md](./syntax-reference.md), maybe that file has already some content? > Check also [syntax-reference.md](./syntax-reference.md)
--- ---
Some sections added ad-hoc, to be integrated later 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 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 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:
---
```

View File

@ -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 Chapter \.d+ ...` is equivalent to `/: Chapter \.d+ ...`
- `/:files.` --> `/:.` e.g. `/:files. ... \-D+.md` is equivalent to `/:. ... \-D+.md` - `/:files.` --> `/:.` e.g. `/:files. ... \-D+.md` is equivalent to `/:. ... \-D+.md`
- `/folders` --> `/` e.g. `/folders Archive...` is equivalent to `/ Archive...` - `/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`: Additional shorter equivalents to allow single-liners like `sorting-spec: \< a-z`:
- `order-asc:` --> `\<` e.g. `order-asc: modified` is equivalent to `\< modified` - `order-asc:` --> `\<` e.g. `order-asc: modified` is equivalent to `\< modified`

View File

@ -7,7 +7,8 @@ export enum CustomSortGroupType {
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
ExactSuffix, ExactSuffix,
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title 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 { export enum CustomSortOrder {

View File

@ -11,6 +11,7 @@ import {
} from './custom-sort'; } from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; 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 => { const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
return { 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 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) 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 = { const MockedLoc: Pos = {
@ -879,6 +880,193 @@ describe('determineSortingGroup', () => {
} as FolderItemForSorting); } 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<Starred_PluginInstance> = {
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<Starred_PluginInstance> = {
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<Starred_PluginInstance> = {
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<Starred_PluginInstance> = {
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<Starred_PluginInstance> = {
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', () => { describe('when sort by metadata is involved', () => {
it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => { it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => {
// given // given
@ -1367,34 +1555,10 @@ describe('determineFolderDatesIfNeeded', () => {
}], }],
outsidersGroupIdx: OUTSIDERS_GROUP_IDX outsidersGroupIdx: OUTSIDERS_GROUP_IDX
} }
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
// when // when
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
determineFolderDatesIfNeeded([result], sortSpec, cardinality) determineFolderDatesIfNeeded([result], sortSpec)
// 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)
// then // then
expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME) expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME)
@ -1413,11 +1577,10 @@ describe('determineFolderDatesIfNeeded', () => {
}], }],
outsidersGroupIdx: OUTSIDERS_GROUP_IDX outsidersGroupIdx: OUTSIDERS_GROUP_IDX
} }
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
// when // when
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
determineFolderDatesIfNeeded([result], sortSpec, cardinality) determineFolderDatesIfNeeded([result], sortSpec)
// then // then
expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST) expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST)

View File

@ -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 { import {
CustomSortGroup, CustomSortGroup,
CustomSortGroupType, CustomSortGroupType,
@ -140,7 +152,11 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
return [false, undefined, undefined] 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 groupIdx: number
let determined: boolean = false let determined: boolean = false
let matchedGroup: string | null | undefined let matchedGroup: string | null | undefined
@ -235,6 +251,19 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
} }
} }
break 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: case CustomSortGroupType.MatchAll:
determined = true; determined = true;
break break
@ -360,11 +389,30 @@ export const determineDatesForFolder = (folder: TFolder, now: number): [Modified
return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder] return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder]
} }
export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSorting>, 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<FolderItemForSorting>, sortingSpec: CustomSortSpec) => {
const Now: number = Date.now() const Now: number = Date.now()
folderItems.forEach((item) => { folderItems.forEach((item) => {
const groupIdx: number | undefined = item.groupIdx const groupIdx: number | undefined = item.groupIdx
if (groupIdx !== undefined && sortingGroupsCardinality[groupIdx] > 1) { if (groupIdx !== undefined) {
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
if (sortOrderNeedsFolderDates(groupOrder)) { if (sortOrderNeedsFolderDates(groupOrder)) {
if (item.folder) { if (item.folder) {
@ -377,8 +425,8 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) { export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer let fileExplorer = this.fileExplorer
const sortingGroupsCardinality: {[key: number]: number} = {}
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app)
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ? const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
this.file.children.filter((entry: TFile | TFolder) => { this.file.children.filter((entry: TFile | TFolder) => {
@ -387,16 +435,14 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
: :
this.file.children) this.file.children)
.map((entry: TFile | TFolder) => { .map((entry: TFile | TFolder) => {
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec) const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, {
const groupIdx: number | undefined = itemForSorting.groupIdx starredPluginInstance: starredPluginInstance
if (groupIdx !== undefined) { })
sortingGroupsCardinality[groupIdx] = 1 + (sortingGroupsCardinality[groupIdx] ?? 0)
}
return itemForSorting return itemForSorting
}) })
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined // 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) { folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
return compareTwoItems(itA, itB, sortingSpec); return compareTwoItems(itA, itB, sortingSpec);

View File

@ -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_SUSPENDED: string = 'custom-sort-icon-suspended'
export const ICON_SORT_ENABLED_NOT_APPLIED: string = 'custom-sort-icon-enabled-not-applied' 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_SYNTAX_ERROR: string = 'custom-sort-icon-syntax-error'
export const ICON_SORT_SUSPENDED_GENERAL_ERROR: string = 'custom-sort-icon-general-error'
export function addIcons() { export function addIcons() {
addIcon(ICON_SORT_ENABLED_ACTIVE, addIcon(ICON_SORT_ENABLED_ACTIVE,
@ -24,6 +25,14 @@ export function addIcons() {
<path d="M 11.096126 55.71973 L 20.217128 41.991214 C 21.134003 40.611175 22.99602 40.235707 24.376058 41.15258 C 24.708624 41.373533 24.99374 41.65865 25.21469 41.991214 L 34.33569 55.71973 C 35.252567 57.09977 34.8771 58.96179 33.49706 59.87866 C 33.005085 60.20552 32.42757 60.37988 31.83691 60.37988 L 13.594907 60.37988 C 11.938053 60.37988 10.594907 59.036736 10.594907 57.37988 C 10.594907 56.78922 10.769266 56.21171 11.096126 55.71973 Z" stroke="red" stroke-width="2" fill="none"/> <path d="M 11.096126 55.71973 L 20.217128 41.991214 C 21.134003 40.611175 22.99602 40.235707 24.376058 41.15258 C 24.708624 41.373533 24.99374 41.65865 25.21469 41.991214 L 34.33569 55.71973 C 35.252567 57.09977 34.8771 58.96179 33.49706 59.87866 C 33.005085 60.20552 32.42757 60.37988 31.83691 60.37988 L 13.594907 60.37988 C 11.938053 60.37988 10.594907 59.036736 10.594907 57.37988 C 10.594907 56.78922 10.769266 56.21171 11.096126 55.71973 Z" stroke="red" stroke-width="2" fill="none"/>
<path d="M 2.5382185 90.37054 L 20.217128 63.76105 C 21.134003 62.38101 22.99602 62.005545 24.376058 62.92242 C 24.708624 63.14337 24.99374 63.428486 25.21469 63.76105 L 42.8936 90.37054 C 43.810475 91.75058 43.435006 93.6126 42.05497 94.52947 C 41.562993 94.85633 40.985477 95.03069 40.39482 95.03069 L 5.0369993 95.03069 C 3.380145 95.03069 2.0369993 93.68755 2.0369993 92.03069 C 2.0369993 91.44004 2.2113584 90.86252 2.5382185 90.37054 Z" stroke="red" stroke-width="2" fill="none"/> <path d="M 2.5382185 90.37054 L 20.217128 63.76105 C 21.134003 62.38101 22.99602 62.005545 24.376058 62.92242 C 24.708624 63.14337 24.99374 63.428486 25.21469 63.76105 L 42.8936 90.37054 C 43.810475 91.75058 43.435006 93.6126 42.05497 94.52947 C 41.562993 94.85633 40.985477 95.03069 40.39482 95.03069 L 5.0369993 95.03069 C 3.380145 95.03069 2.0369993 93.68755 2.0369993 92.03069 C 2.0369993 91.44004 2.2113584 90.86252 2.5382185 90.37054 Z" stroke="red" stroke-width="2" fill="none"/>
<path d="M 88.33569 46.24901 L 79.21469 59.97753 C 78.297815 61.35757 76.4358 61.73304 75.05576 60.81616 C 74.72319 60.59521 74.43808 60.310096 74.21713 59.97753 L 65.09613 46.24901 C 64.17925 44.868973 64.55472 43.006957 65.93476 42.09008 C 66.42673 41.76322 67.00425 41.588863 67.59491 41.588863 L 85.83691 41.588863 C 87.49377 41.588863 88.83691 42.93201 88.83691 44.58886 C 88.83691 45.17952 88.66255 45.757036 88.33569 46.24901 Z" fill="red"/> <path d="M 88.33569 46.24901 L 79.21469 59.97753 C 78.297815 61.35757 76.4358 61.73304 75.05576 60.81616 C 74.72319 60.59521 74.43808 60.310096 74.21713 59.97753 L 65.09613 46.24901 C 64.17925 44.868973 64.55472 43.006957 65.93476 42.09008 C 66.42673 41.76322 67.00425 41.588863 67.59491 41.588863 L 85.83691 41.588863 C 87.49377 41.588863 88.83691 42.93201 88.83691 44.58886 C 88.83691 45.17952 88.66255 45.757036 88.33569 46.24901 Z" fill="red"/>
<path d="M 88.33569 77.48964 L 79.21469 91.21816 C 78.297815 92.5982 76.4358 92.97366 75.05576 92.05679 C 74.72319 91.83584 74.43808 91.55072 74.21713 91.21816 L 65.09613 77.48964 C 64.17925 76.1096 64.55472 74.247585 65.93476 73.33071 C 66.42673 73.00385 67.00425 72.82949 67.59491 72.82949 L 85.83691 72.82949 C 87.49377 72.82949 88.83691 74.17264 88.83691 75.82949 C 88.83691 76.42015 88.66255 76.99766 88.33569 77.48964 Z" fill="red"/>`
)
addIcon(ICON_SORT_SUSPENDED_GENERAL_ERROR,
`<path d="M 93.54751 9.983795 L 79.21469 31.556912 C 78.297815 32.93695 76.4358 33.31242 75.05576 32.395544 C 74.72319 32.174593 74.43808 31.88948 74.21713 31.556912 L 59.8843 9.983795 C 58.96743 8.603756 59.3429 6.74174 60.722935 5.824865 C 61.21491 5.4980047 61.792426 5.3236456 62.383084 5.3236456 L 91.04873 5.3236456 C 92.70559 5.3236456 94.04873 6.666791 94.04873 8.323646 C 94.04873 8.914304 93.87437 9.49182 93.54751 9.983795 Z" fill="red"/>
<path d="M 11.096126 32.678017 L 20.217128 18.949499 C 21.134003 17.56946 22.99602 17.193992 24.376058 18.110867 C 24.708624 18.331818 24.99374 18.616933 25.21469 18.949499 L 34.33569 32.678017 C 35.252567 34.058055 34.8771 35.92007 33.49706 36.836947 C 33.005085 37.163807 32.42757 37.338166 31.83691 37.338166 L 13.594907 37.338166 C 11.938053 37.338166 10.594907 35.99502 10.594907 34.338166 C 10.594907 33.747508 10.769266 33.16999 11.096126 32.678017 Z" fill="red"/>
<path d="M 11.096126 55.71973 L 20.217128 41.991214 C 21.134003 40.611175 22.99602 40.235707 24.376058 41.15258 C 24.708624 41.373533 24.99374 41.65865 25.21469 41.991214 L 34.33569 55.71973 C 35.252567 57.09977 34.8771 58.96179 33.49706 59.87866 C 33.005085 60.20552 32.42757 60.37988 31.83691 60.37988 L 13.594907 60.37988 C 11.938053 60.37988 10.594907 59.036736 10.594907 57.37988 C 10.594907 56.78922 10.769266 56.21171 11.096126 55.71973 Z" fill="red"/>
<path d="M 2.5382185 90.37054 L 20.217128 63.76105 C 21.134003 62.38101 22.99602 62.005545 24.376058 62.92242 C 24.708624 63.14337 24.99374 63.428486 25.21469 63.76105 L 42.8936 90.37054 C 43.810475 91.75058 43.435006 93.6126 42.05497 94.52947 C 41.562993 94.85633 40.985477 95.03069 40.39482 95.03069 L 5.0369993 95.03069 C 3.380145 95.03069 2.0369993 93.68755 2.0369993 92.03069 C 2.0369993 91.44004 2.2113584 90.86252 2.5382185 90.37054 Z" fill="red"/>
<path d="M 88.33569 46.24901 L 79.21469 59.97753 C 78.297815 61.35757 76.4358 61.73304 75.05576 60.81616 C 74.72319 60.59521 74.43808 60.310096 74.21713 59.97753 L 65.09613 46.24901 C 64.17925 44.868973 64.55472 43.006957 65.93476 42.09008 C 66.42673 41.76322 67.00425 41.588863 67.59491 41.588863 L 85.83691 41.588863 C 87.49377 41.588863 88.83691 42.93201 88.83691 44.58886 C 88.83691 45.17952 88.66255 45.757036 88.33569 46.24901 Z" fill="red"/>
<path d="M 88.33569 77.48964 L 79.21469 91.21816 C 78.297815 92.5982 76.4358 92.97366 75.05576 92.05679 C 74.72319 91.83584 74.43808 91.55072 74.21713 91.21816 L 65.09613 77.48964 C 64.17925 76.1096 64.55472 74.247585 65.93476 73.33071 C 66.42673 73.00385 67.00425 72.82949 67.59491 72.82949 L 85.83691 72.82949 C 87.49377 72.82949 88.83691 74.17264 88.83691 75.82949 C 88.83691 76.42015 88.66255 76.99766 88.33569 77.48964 Z" fill="red"/>` <path d="M 88.33569 77.48964 L 79.21469 91.21816 C 78.297815 92.5982 76.4358 92.97366 75.05576 92.05679 C 74.72319 91.83584 74.43808 91.55072 74.21713 91.21816 L 65.09613 77.48964 C 64.17925 76.1096 64.55472 74.247585 65.93476 73.33071 C 66.42673 73.00385 67.00425 72.82949 67.59491 72.82949 L 85.83691 72.82949 C 87.49377 72.82949 88.83691 74.17264 88.83691 75.82949 C 88.83691 76.42015 88.66255 76.99766 88.33569 77.48964 Z" fill="red"/>`
) )
addIcon(ICON_SORT_ENABLED_NOT_APPLIED, addIcon(ICON_SORT_ENABLED_NOT_APPLIED,

View File

@ -29,6 +29,9 @@ target-folder: tricky folder
< a-z by-metadata: Some-dedicated-field < a-z by-metadata: Some-dedicated-field
with-metadata: Pages with-metadata: Pages
> a-z by-metadata: > a-z by-metadata:
starred:
/:files starred:
/folders starred:
:::: Conceptual model :::: Conceptual model
/: Entities /: Entities
@ -82,6 +85,9 @@ target-folder: tricky folder 2
< a-z by-metadata: Some-dedicated-field < a-z by-metadata: Some-dedicated-field
% with-metadata: Pages % with-metadata: Pages
> a-z by-metadata: > a-z by-metadata:
/folders:files starred:
/:files starred:
/folders starred:
:::: Conceptual model :::: Conceptual model
/:files Entities /:files Entities
@ -165,11 +171,22 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: 'Pages', withMetadataFieldName: 'Pages',
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse 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, order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders type: CustomSortGroupType.Outsiders
}], }],
outsidersGroupIdx: 2, outsidersGroupIdx: 5,
targetFoldersPaths: [ targetFoldersPaths: [
'tricky folder 2' 'tricky folder 2'
] ]

View File

@ -188,12 +188,15 @@ const FilesWithExtGroupShortLexeme: string = '/:.'
const FoldersGroupVerboseLexeme: string = '/folders' const FoldersGroupVerboseLexeme: string = '/folders'
const FoldersGroupShortLexeme: string = '/' const FoldersGroupShortLexeme: string = '/'
const AnyTypeGroupLexemeShort: string = '%' // See % as a combination of / and : 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 HideItemShortLexeme: string = '--%' // See % as a combination of / and :
const HideItemVerboseLexeme: string = '/--hide:' const HideItemVerboseLexeme: string = '/--hide:'
const MetadataFieldIndicatorLexeme: string = 'with-metadata:' const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
const StarredItemsIndicatorLexeme: string = 'starred:'
const CommentPrefix: string = '//' const CommentPrefix: string = '//'
const PriorityModifierPrio1Lexeme: string = '/!' const PriorityModifierPrio1Lexeme: string = '/!'
@ -232,7 +235,8 @@ const SortingGroupPrefixes: { [key: string]: SortingGroupType } = {
[FoldersGroupShortLexeme]: {foldersOnly: true}, [FoldersGroupShortLexeme]: {foldersOnly: true},
[FoldersGroupVerboseLexeme]: {foldersOnly: true}, [FoldersGroupVerboseLexeme]: {foldersOnly: true},
[AnyTypeGroupLexemeShort]: {}, [AnyTypeGroupLexemeShort]: {},
[AnyTypeGroupLexeme]: {}, [AnyTypeGroupLexeme1]: {},
[AnyTypeGroupLexeme2]: {},
[HideItemShortLexeme]: {itemToHide: true}, [HideItemShortLexeme]: {itemToHide: true},
[HideItemVerboseLexeme]: {itemToHide: true} [HideItemVerboseLexeme]: {itemToHide: true}
} }
@ -1337,6 +1341,13 @@ export class SortingSpecProcessor {
foldersOnly: spec.foldersOnly, foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt matchFilenameWithExt: spec.matchFilenameWithExt
} }
} else if (theOnly.startsWith(StarredItemsIndicatorLexeme)) {
return {
type: CustomSortGroupType.StarredOnly,
filesOnly: spec.filesOnly,
foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt
}
} else { } else {
// For non-three dots single text line assume exact match group // For non-three dots single text line assume exact match group
return { return {

View File

@ -23,6 +23,7 @@ import {
ICON_SORT_ENABLED_ACTIVE, ICON_SORT_ENABLED_ACTIVE,
ICON_SORT_ENABLED_NOT_APPLIED, ICON_SORT_ENABLED_NOT_APPLIED,
ICON_SORT_SUSPENDED, ICON_SORT_SUSPENDED,
ICON_SORT_SUSPENDED_GENERAL_ERROR,
ICON_SORT_SUSPENDED_SYNTAX_ERROR ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons"; } 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 // Safe to suspend when suspended and re-enable when enabled
switchPluginStateTo(enabled: boolean, updateRibbonBtnIcon: boolean = true) { 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.settings.suspended = !enabled;
this.saveSettings() this.saveSettings()
let iconToSet: string let iconToSet: string
@ -136,20 +168,24 @@ export default class CustomSortPlugin extends Plugin {
} else { } else {
this.readAndParseSortingSpec(); this.readAndParseSortingSpec();
if (this.sortSpecCache) { if (this.sortSpecCache) {
this.showNotice('Custom sort ON'); if (fileExplorerView) {
this.initialAutoOrManualSortingTriggered = true this.showNotice('Custom sort ON');
iconToSet = ICON_SORT_ENABLED_ACTIVE 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 { } else {
iconToSet = ICON_SORT_SUSPENDED_SYNTAX_ERROR iconToSet = ICON_SORT_SUSPENDED_SYNTAX_ERROR
this.settings.suspended = true this.settings.suspended = true
this.saveSettings() this.saveSettings()
} }
} }
const fileExplorerView: FileExplorerView | undefined = this.getFileExplorer()
if (fileExplorerView) { if (fileExplorerView) {
if (!this.fileExplorerFolderPatched) {
this.fileExplorerFolderPatched = this.patchFileExplorerFolder(fileExplorerView);
}
if (this.fileExplorerFolderPatched) { if (this.fileExplorerFolderPatched) {
fileExplorerView.requestSort(); fileExplorerView.requestSort();
} }
@ -215,7 +251,7 @@ export default class CustomSortPlugin extends Plugin {
this.initialAutoOrManualSortingTriggered = true this.initialAutoOrManualSortingTriggered = true
if (this.sortSpecCache) { // successful read of sorting specifications? if (this.sortSpecCache) { // successful read of sorting specifications?
this.showNotice('Custom sort ON') this.showNotice('Custom sort ON')
const fileExplorerView: FileExplorerView | undefined = this.getFileExplorer() const fileExplorerView: FileExplorerView | undefined = this.checkFileExplorerIsAvailableAndPatchable(false)
if (fileExplorerView) { if (fileExplorerView) {
setIcon(this.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE) setIcon(this.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
fileExplorerView.requestSort() 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 // 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; let plugin = this;
fileExplorer = fileExplorer ?? this.getFileExplorer() // patching file explorer might fail here because of various non-error reasons.
if (fileExplorer) { // That's why not showing and not logging error message here
patchableFileExplorer = patchableFileExplorer ?? this.checkFileExplorerIsAvailableAndPatchable(false)
if (patchableFileExplorer) {
// @ts-ignore // @ts-ignore
let tmpFolder = new TFolder(Vault, ""); let tmpFolder = new TFolder(Vault, "");
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor; let Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, { const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) { sort(old: any) {
return function (...args: any[]) { return function (...args: any[]) {

20
src/types/types.d.ts vendored
View File

@ -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 // Needed to support monkey-patching of the folder sort() function
@ -7,10 +7,28 @@ declare module 'obsidian' {
viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>; viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>;
} }
// undocumented internal interface - for experimental features
export interface PluginInstance {
id: string;
}
export interface App { export interface App {
internalPlugins: InternalPlugins; // undocumented internal API - for experimental features
viewRegistry: ViewRegistry; 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<string, InstalledPlugin>;
getPluginById(id: string): InstalledPlugin;
}
interface FileExplorerFolder { interface FileExplorerFolder {
} }

View File

@ -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
}