Skip to content

Commit 7bec656

Browse files
committed
feat: sorting options
1 parent cbf33a9 commit 7bec656

File tree

8 files changed

+231
-39
lines changed

8 files changed

+231
-39
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ indent_size = 2
88
indent_style = space
99
max_line_length = 120
1010
insert_final_newline = true
11+
<<<<<<< HEAD
1112
trim_trailing_whitespace = true
1213

1314
[*.{diff,md,mdx}]
1415
trim_trailing_whitespace = false
16+
=======
17+
indent_style = space
18+
indent_size = 2
19+
>>>>>>> f29c692 (feat: sorting options)

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
npm node_modules
2-
build
2+
build
3+
main.js

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,60 @@ Waypoint is an Obsidian plugin that automatically generates tables of contents/M
3232

3333
Note that since waypoints can only be generated in folder notes, **it is highly recommended that you install a plugin like [Folder Note](https://github.com/xpgo/obsidian-folder-note-plugin)** to ensure that folder notes don't get lost after folder renames and other actions that change the file tree. If a folder note containing a waypoint is renamed to something other than the folder's name, the waypoint will no longer be updated.
3434

35+
### Ordering Notes on Waypoint
36+
37+
Waypoint provides the following methods for sorting your Notes:
38+
39+
- Natural
40+
- Lexicographic
41+
- Priority
42+
- Folders First
43+
44+
#### Natural Sort Order
45+
46+
This is the default setting. Filenames are sorted alphabetically, except numeric portions are sorted in numerical order.
47+
48+
```text
49+
- Note
50+
- Note1
51+
- Note2
52+
- Note100
53+
```
54+
55+
#### Lexicograhic Order
56+
57+
Filenames are sorted entirely alphabetically.
58+
59+
```text
60+
- Note
61+
- Note1
62+
- Note100
63+
- Note2
64+
```
65+
66+
#### Folders First
67+
68+
- All folders appear at the top of the list and are sorted in a natural sort order
69+
- Files appear after any folders and sorted in natural sort order
70+
71+
#### Priority Sort Order
72+
73+
By default, notes in Waypoint are sorted alphabetically. However, with priority sort enabled, you can organize your notes in a specific order by enabling setting their Waypoint priorities. To do this, you would add an entry in your [note metadata](https://help.obsidian.md/Editing+and+formatting/Metadata) with a numeric value, such as:
74+
75+
```markdown
76+
---
77+
waypointPriority: 0
78+
---
79+
80+
# My note
81+
```
82+
83+
Waypoint will then sort the notes based on the numeric value assigned to `waypointPriority`. Smaller values correspond to higher priority, so a note with `waypointPriority: 0` will appear above a note with `waypointPriority: 1`. If two notes have the same priority, they will be sorted alphabetically.
84+
85+
In case a note does not have a `waypointPriority` defined, it will fall back to the default alphabetical sorting strategy.
86+
87+
The `waypointPriority` key can be customized to a different key label in the Waypoint settings. This functionality works for both regular notes and folder notes.
88+
3589
## Current Limitations
3690

3791
- **Waypoints can only be created within a folder note**

main.ts

Lines changed: 154 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ enum FolderNoteType {
1616
OutsideFolder = "OUTSIDE_FOLDER",
1717
}
1818

19+
enum SortType {
20+
Priority = "PRIORITY",
21+
Natural = "NATURAL",
22+
Lexicographic = "LEXICOGRAPHIC",
23+
FoldersFirst = "FOLDERS_FIRST",
24+
}
25+
1926
interface WaypointSettings {
2027
waypointFlag: string;
2128
stopScanAtFolderNotes: boolean;
@@ -25,6 +32,8 @@ interface WaypointSettings {
2532
useWikiLinks: boolean;
2633
showEnclosingNote: boolean;
2734
folderNoteType: string;
35+
waypointPriorityKey: string;
36+
sortType: string;
2837
useSpaces: boolean;
2938
numSpaces: number;
3039
ignoredFolders: string[];
@@ -40,6 +49,8 @@ const DEFAULT_SETTINGS: WaypointSettings = {
4049
useWikiLinks: true,
4150
showEnclosingNote: false,
4251
folderNoteType: FolderNoteType.InsideFolder,
52+
waypointPriorityKey: "waypointPriority",
53+
sortType: SortType.Natural,
4354
useSpaces: false,
4455
numSpaces: 2,
4556
ignoredFolders: ["Templates"],
@@ -85,7 +96,14 @@ export default class Waypoint extends Plugin {
8596
this.scheduleUpdate();
8697
})
8798
);
88-
this.registerEvent(this.app.vault.on("modify", this.detectWaypointFlag));
99+
this.registerEvent(
100+
this.app.vault.on("modify", (file) => {
101+
this.log("modify " + file.name);
102+
this.foldersWithChanges.add(file.parent);
103+
this.scheduleUpdate();
104+
this.detectWaypointFlag(file as TFile);
105+
})
106+
);
89107
});
90108

91109
// This adds a settings tab so the user can configure various aspects of the plugin
@@ -113,7 +131,7 @@ export default class Waypoint extends Plugin {
113131
* Scan the given file for the waypoint flag. If found, update the waypoint.
114132
* @param file The file to scan
115133
*/
116-
detectWaypointFlag = async (file: TFile) => {
134+
async detectWaypointFlag(file: TFile): Promise<void> {
117135
this.log("Modification on " + file.name);
118136
this.log("Scanning for Waypoint flags...");
119137
const text = await this.app.vault.cachedRead(file);
@@ -123,7 +141,7 @@ export default class Waypoint extends Plugin {
123141
if (this.isFolderNote(file)) {
124142
this.log("Found waypoint flag in folder note!");
125143
await this.updateWaypoint(file);
126-
await this.updateParentWaypoint(file.parent, this.settings.folderNoteType === FolderNoteType.OutsideFolder);
144+
await this.updateAncestorWaypoint(file.parent, this.settings.folderNoteType === FolderNoteType.OutsideFolder);
127145
return;
128146
} else if (file.parent.isRoot()) {
129147
this.log("Found waypoint flag in root folder.");
@@ -142,7 +160,7 @@ export default class Waypoint extends Plugin {
142160
}
143161
}
144162
this.log("No waypoint flags found.");
145-
};
163+
}
146164

147165
isFolderNote(file: TFile): boolean {
148166
if (this.settings.folderNoteType === FolderNoteType.InsideFolder) {
@@ -186,7 +204,7 @@ export default class Waypoint extends Plugin {
186204
* Given a file with a waypoint flag, generate a file tree representation and update the waypoint text.
187205
* @param file The file to update
188206
*/
189-
async updateWaypoint(file: TFile) {
207+
async updateWaypoint(file: TFile): Promise<void> {
190208
this.log("Updating waypoint in " + file.path);
191209
let fileTree;
192210
if (this.settings.folderNoteType === FolderNoteType.InsideFolder) {
@@ -220,8 +238,17 @@ export default class Waypoint extends Plugin {
220238
return;
221239
}
222240
this.log("Waypoint found at " + waypointStart + " to " + waypointEnd);
223-
lines.splice(waypointStart, waypointEnd !== -1 ? waypointEnd - waypointStart + 1 : 1, waypoint);
224-
await this.app.vault.modify(file, lines.join("\n"));
241+
242+
// Get the current waypoint block from lines and join it to form a string
243+
const currentWaypoint =
244+
waypointEnd !== -1 ? lines.slice(waypointStart, waypointEnd + 1).join("\n") : lines[waypointStart];
245+
246+
// Only splice and modify if waypoint differs from the current block
247+
if (currentWaypoint !== waypoint) {
248+
this.log("Waypoint content changed, updating");
249+
lines.splice(waypointStart, waypointEnd !== -1 ? waypointEnd - waypointStart + 1 : 1, waypoint);
250+
await this.app.vault.modify(file, lines.join("\n"));
251+
}
225252
}
226253

227254
/**
@@ -241,7 +268,7 @@ export default class Waypoint extends Plugin {
241268
const indent = this.settings.useSpaces ? " ".repeat(this.settings.numSpaces) : " ";
242269
const bullet = indent.repeat(indentLevel) + "-";
243270
if (node instanceof TFile) {
244-
console.log(node);
271+
this.log(node);
245272
// Print the file name
246273
// Check for the parent being the root because otherwise the "root note" would be included in the tree
247274
if (node.extension == "md" && !node.parent.isRoot()) {
@@ -295,12 +322,21 @@ export default class Waypoint extends Plugin {
295322
if (node.children && node.children.length > 0) {
296323
// Print the files and nested folders within the folder
297324
let children = node.children;
298-
children = children.sort((a, b) => {
299-
return a.name.localeCompare(b.name, undefined, {
300-
numeric: true,
301-
sensitivity: "base",
302-
});
303-
});
325+
326+
switch (this.settings.sortType) {
327+
case SortType.Lexicographic:
328+
children = children.sort();
329+
break;
330+
case SortType.Priority:
331+
children = children.sort(this.sortWithPriority);
332+
break;
333+
case SortType.FoldersFirst:
334+
children = children.sort(this.sortFoldersFirst);
335+
break;
336+
default:
337+
children = children.sort(this.sortWithNaturalOrder);
338+
break;
339+
}
304340
if (!this.settings.showFolderNotes) {
305341
if (this.settings.folderNoteType === FolderNoteType.InsideFolder) {
306342
children = children.filter((child) => this.settings.showFolderNotes || child.name !== node.name + ".md");
@@ -350,31 +386,31 @@ export default class Waypoint extends Plugin {
350386
/**
351387
* Scan the changed folders and their ancestors for waypoints and update them if found.
352388
*/
353-
updateChangedFolders = async () => {
389+
async updateChangedFolders() {
354390
this.log("Updating changed folders...");
355391
this.foldersWithChanges.forEach((folder) => {
356392
this.log("Updating " + folder.path);
357-
this.updateParentWaypoint(folder, true);
393+
this.updateAncestorWaypoint(folder, true);
358394
});
359395
this.foldersWithChanges.clear();
360-
};
396+
}
361397

362398
/**
363399
* Schedule an update for the changed folders after debouncing to prevent excessive updates.
364400
*/
365401
scheduleUpdate = debounce(this.updateChangedFolders.bind(this), 500, true);
366402

367403
/**
368-
* Update the ancestor waypoint (if any) of the given file/folder.
404+
* Update all ancestor waypoints (if any) of the given file/folder.
369405
* @param node The node to start the search from
370406
* @param includeCurrentNode Whether to include the given folder in the search
371407
*/
372-
updateParentWaypoint = async (node: TAbstractFile, includeCurrentNode: boolean) => {
408+
async updateAncestorWaypoint(node: TAbstractFile, includeCurrentNode: boolean): Promise<void> {
373409
const parentWaypoint = await this.locateParentWaypoint(node, includeCurrentNode);
374410
if (parentWaypoint !== null) {
375411
this.updateWaypoint(parentWaypoint);
376412
}
377-
};
413+
}
378414

379415
/**
380416
* Locate the ancestor waypoint (if any) of the given file/folder.
@@ -383,10 +419,10 @@ export default class Waypoint extends Plugin {
383419
* @returns The ancestor waypoint, or null if none was found
384420
*/
385421
async locateParentWaypoint(node: TAbstractFile, includeCurrentNode: boolean): Promise<TFile> {
386-
this.log("Locating parent waypoint of " + node.name);
422+
this.log("Locating all ancestor waypoints of " + node.name);
387423
let folder = includeCurrentNode ? node : node.parent;
388424
// When there's a root-level folder note
389-
if (node.parent.isRoot() && this.settings.root !== null) {
425+
if (node.parent?.isRoot() && this.settings.root !== null) {
390426
const file = this.app.vault.getAbstractFileByPath(this.settings.root);
391427
if (file instanceof TFile) {
392428
this.log("Found folder note: " + file.path);
@@ -435,7 +471,7 @@ export default class Waypoint extends Plugin {
435471
}
436472
}
437473

438-
log(message: string) {
474+
log(message?: string | TFile) {
439475
if (this.settings.debugLogging) {
440476
console.log(message);
441477
}
@@ -448,6 +484,71 @@ export default class Waypoint extends Plugin {
448484
async saveSettings() {
449485
await this.saveData(this.settings);
450486
}
487+
488+
getWaypointPriority(file: TAbstractFile): number | null {
489+
if (file instanceof TFile) {
490+
const { waypointPriorityKey } = this.settings;
491+
const fileCache = this.app.metadataCache.getFileCache(file as TFile);
492+
if (typeof fileCache?.frontmatter?.[waypointPriorityKey] === "number") {
493+
return fileCache.frontmatter[waypointPriorityKey];
494+
} else {
495+
return null;
496+
}
497+
} else if (file instanceof TFolder) {
498+
if (this.settings.folderNoteType === FolderNoteType.InsideFolder) {
499+
// If file is a folder and folder note is an inside note, attempt to find a child note with the same name.
500+
const foldernote: TAbstractFile | null = file.children.find(
501+
(child) => child instanceof TFile && child.basename === file.name
502+
);
503+
return foldernote ? this.getWaypointPriority(foldernote) : null;
504+
} else if (this.settings.folderNoteType === FolderNoteType.OutsideFolder) {
505+
// If file is a folder and folder note is an outside note, attempt to find a sibling note with the same name.
506+
if (!file.isRoot()) {
507+
const foldernote: TAbstractFile | null = file.parent.children.find(
508+
(child) => child instanceof TFile && child.basename === file.name
509+
);
510+
return foldernote ? this.getWaypointPriority(foldernote) : null;
511+
} else {
512+
return null; // Handle case when the file is the root folder.
513+
}
514+
}
515+
return null;
516+
}
517+
}
518+
519+
sortWithNaturalOrder = (a: TAbstractFile, b: TAbstractFile): number => {
520+
return a.name.localeCompare(b.name, undefined, {
521+
numeric: true,
522+
sensitivity: "base",
523+
});
524+
};
525+
526+
sortFoldersFirst = (a: TAbstractFile, b: TAbstractFile): number => {
527+
if (a instanceof TFolder) {
528+
// if b is also a folder, sort normally, otherwise a (the folder) comes first
529+
return b instanceof TFolder ? this.sortWithNaturalOrder(a, b) : -1;
530+
}
531+
// a is a file. if b is a folder, it comes first, otherwise sort both files normally
532+
return b instanceof TFolder ? 1 : this.sortWithNaturalOrder(a, b);
533+
};
534+
535+
sortWithPriority = (a: TAbstractFile, b: TAbstractFile): number => {
536+
const aPriority = this.getWaypointPriority(a);
537+
const bPriority = this.getWaypointPriority(b);
538+
if (aPriority !== null && bPriority !== null) {
539+
// If both have waypointPriority, the one with a lower priority number should come first.
540+
return aPriority - bPriority;
541+
} else if (aPriority !== null) {
542+
// If only `a` has waypointPriority, `a` should come first.
543+
return -1;
544+
} else if (bPriority !== null) {
545+
// If only `b` has waypointPriority, `b` should come first.
546+
return 1;
547+
} else {
548+
// If neither has priority, sort alphabetically.
549+
return this.sortWithNaturalOrder(a, b);
550+
}
551+
};
451552
}
452553

453554
class WaypointSettingsTab extends PluginSettingTab {
@@ -473,6 +574,7 @@ class WaypointSettingsTab extends PluginSettingTab {
473574
.onChange(async (value) => {
474575
this.plugin.settings.folderNoteType = value;
475576
await this.plugin.saveSettings();
577+
this.display();
476578
})
477579
);
478580
new Setting(containerEl)
@@ -573,6 +675,35 @@ class WaypointSettingsTab extends PluginSettingTab {
573675
await this.plugin.saveSettings();
574676
})
575677
);
678+
new Setting(this.containerEl)
679+
.setName("Sorting Method")
680+
.setDesc("Select how you would like to have your waypoint lists sorted.")
681+
.addDropdown((dropdown) =>
682+
dropdown
683+
.addOption(SortType.Natural, "Natural")
684+
.addOption(SortType.Priority, "Prioritized")
685+
.addOption(SortType.Lexicographic, "Lexicographic")
686+
.addOption(SortType.FoldersFirst, "Folders First")
687+
.setValue(this.plugin.settings.sortType)
688+
.onChange(async (value) => {
689+
this.plugin.settings.sortType = value;
690+
await this.plugin.saveSettings();
691+
this.display();
692+
})
693+
);
694+
new Setting(containerEl)
695+
.setName("Frontmatter key for note priority")
696+
.setDesc("The frontmatter key to set the note order piority when listed in a Waypoint.")
697+
.addText((text) =>
698+
text
699+
.setPlaceholder(DEFAULT_SETTINGS.waypointPriorityKey)
700+
.setValue(this.plugin.settings.waypointPriorityKey)
701+
.onChange(async (value) => {
702+
this.plugin.settings.waypointPriorityKey = value;
703+
await this.plugin.saveSettings();
704+
})
705+
)
706+
.setDisabled(this.plugin.settings.sortType !== SortType.Priority);
576707
new Setting(containerEl)
577708
.setName("Ignored folders")
578709
.setDesc("Folders that Waypoint should ignore")

0 commit comments

Comments
 (0)