#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.
- 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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:
---
```

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. ... \-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`

View File

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

View File

@ -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<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', () => {
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)

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 {
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<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()
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<FolderItemForSor
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer
const sortingGroupsCardinality: {[key: number]: number} = {}
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app)
const folderItems: Array<FolderItemForSorting> = (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);

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_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() {
<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 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"/>`
)
addIcon(ICON_SORT_ENABLED_NOT_APPLIED,

View File

@ -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'
]

View File

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

View File

@ -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[]) {

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
@ -7,10 +7,28 @@ declare module 'obsidian' {
viewByType: Record<string, (leaf: WorkspaceLeaf) => 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<string, InstalledPlugin>;
getPluginById(id: string): InstalledPlugin;
}
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
}