Skip to content

Commit 274ed51

Browse files
committed
Add in-folder variant of quick switcher
1 parent 0c7caf7 commit 274ed51

File tree

9 files changed

+276
-34
lines changed

9 files changed

+276
-34
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,24 @@ This Obsidian plugin provides commands that improve navigation between the headi
22

33
## Commands
44

5-
- **Next/Previous Heading**: Go to the next/previous heading, based on the current cursor position.
6-
- **Open Switcher**: Open a quick switcher that presents the document's outline and allows you to quickly select a heading to jump to.
5+
- **Next/Previous heading**: Go to the next/previous heading, based on the current cursor position.
6+
- **Open switcher**: Open a quick switcher that presents the document's outline and allows you to quickly select a heading to jump to.
7+
- **In folder:
8+
- **In folder: Open switcher**: Open a quick switcher that presents the outlines of all the documents in the current folder, and allows you to quickly select a heading to jump to.
79

810
## How to install
911

1012
Use one of the following methods:
1113

1214
- Install it from Obsidian Community plugins.
1315
- Manually copy the released `main.js`, and `manifest.json` to `<your vault>/.obsidian/plugins/obsidian-gotoheading`.
16+
17+
## What's new?
18+
19+
### Version 0.2.0
20+
21+
- Add the _In folder_ variant of the _Open switcher_ command.
22+
23+
### Version 0.1.1
24+
25+
- Initial release

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "oin-gotoheading",
33
"name": "Go To Heading",
4-
"version": "0.1.1",
4+
"version": "0.2.0",
55
"minAppVersion": "1.4.11",
66
"description": "Quickly navigate between headings",
77
"author": "join",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oin-gotoheading",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "Quickly navigate between headings in Obisidian",
55
"main": "main.js",
66
"scripts": {

src/heading_modal.ts

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,34 @@
11
import { App, Editor, FuzzyMatch, FuzzySuggestModal, Notice, setIcon } from "obsidian";
22
import { GotoHeadingSettings } from "./settings";
33

4-
interface HeadingSuggestion {
4+
export enum SuggestionType {
5+
File,
6+
Heading,
7+
};
8+
9+
export interface FileSuggestion {
10+
type: SuggestionType;
11+
text: string;
12+
13+
path: string;
14+
}
15+
16+
export interface HeadingSuggestion {
17+
type: SuggestionType;
18+
text: string;
19+
520
heading: string;
621
level: number;
722
line: number;
23+
file?: FileSuggestion;
824
}
925

10-
type HeadingModalCallback = (item: HeadingSuggestion) => void;
26+
export type Suggestion = HeadingSuggestion | FileSuggestion;
1127

12-
export class HeadingModal extends FuzzySuggestModal<HeadingSuggestion> {
13-
public items: HeadingSuggestion[] = [];
28+
type HeadingModalCallback = (item: Suggestion) => void;
29+
30+
export class HeadingModal extends FuzzySuggestModal<Suggestion> {
31+
public items: Suggestion[] = [];
1432
public defaultItemIndex: number = -1;
1533
public onChoose: HeadingModalCallback;
1634
public settings: GotoHeadingSettings;
@@ -33,28 +51,59 @@ export class HeadingModal extends FuzzySuggestModal<HeadingSuggestion> {
3351
}
3452
}
3553

36-
getItems(): HeadingSuggestion[] {
54+
getItems(): Suggestion[] {
3755
return this.items;
3856
}
3957

40-
getItemText(item: HeadingSuggestion): string {
41-
return item.heading;
58+
getItemText(item: Suggestion): string {
59+
return item.text;
4260
}
4361

44-
onChooseItem(item: HeadingSuggestion, evt: MouseEvent | KeyboardEvent): void {
62+
onChooseItem(item: Suggestion, evt: MouseEvent | KeyboardEvent): void {
4563
this.onChoose?.(item);
4664
}
4765

48-
renderSuggestion(item: FuzzyMatch<HeadingSuggestion>, el: HTMLElement): void {
49-
const isSearching = this.inputEl.value.length > 0;
50-
const level = item.item.level;
51-
const iconName = level >= 1 && level <= 6 ? `heading-${level}` : "heading";
52-
66+
renderSuggestion(item: FuzzyMatch<Suggestion>, el: HTMLElement): void {
5367
el.classList.add("join-gotoheading-headingmodal-suggestion");
5468

5569
if(this.settings.highlightCurrentHeading && this.defaultItemIndex >= 0 && this.items.indexOf(item.item) == this.defaultItemIndex) {
5670
el.classList.add("join-gotoheading-headingmodal-suggestion-default");
5771
}
72+
73+
switch(item.item.type) {
74+
case SuggestionType.File:
75+
return this.renderFileSuggestion(item, el);
76+
case SuggestionType.Heading:
77+
return this.renderHeadingSuggestion(item, el);
78+
}
79+
}
80+
81+
protected renderFileSuggestion(item: FuzzyMatch<Suggestion>, el: HTMLElement) {
82+
const s = item.item as FileSuggestion;
83+
84+
const isSearching = this.inputEl.value.length > 0;
85+
86+
el.classList.add("join-gotoheading-headingmodal-suggestion");
87+
el.classList.add("join-gotoheading-headingmodal-suggestion-file");
88+
if(isSearching) {
89+
el.classList.add("join-gotoheading-headingmodal-suggestion-searching");
90+
}
91+
92+
// setIcon(el, "file-text");
93+
94+
const titleEl = el.createSpan({ cls: "title"});
95+
super.renderSuggestion(item, titleEl);
96+
97+
let smallEl = el.createEl("small", { cls: "icon" });
98+
setIcon(smallEl, "file-text");
99+
}
100+
101+
protected renderHeadingSuggestion(item: FuzzyMatch<Suggestion>, el: HTMLElement) {
102+
const s = item.item as HeadingSuggestion;
103+
104+
const isSearching = this.inputEl.value.length > 0;
105+
const level = s.level;
106+
const iconName = level >= 1 && level <= 6 ? `heading-${level}` : "heading";
58107

59108
if(isSearching) {
60109
// Set a heading icon
@@ -73,7 +122,7 @@ export class HeadingModal extends FuzzySuggestModal<HeadingSuggestion> {
73122

74123
// If a search is ongoing, display parent heading information
75124
if(isSearching) {
76-
let smallEl = el.createEl("small", { text: this.parentHeadingString(item.item), cls: "path" });
125+
el.createEl("small", { text: this.parentHeadingString(s), cls: "path" });
77126
} else {
78127
let smallEl = el.createEl("small", { cls: "icon" });
79128
setIcon(smallEl, iconName);
@@ -83,20 +132,27 @@ export class HeadingModal extends FuzzySuggestModal<HeadingSuggestion> {
83132
protected parentHeadingString(item: HeadingSuggestion): string {
84133
let string = "";
85134

135+
if(item.file) {
136+
string = item.file.text;
137+
}
138+
86139
while(item) {
87140
const parentIndex = this.items.findLastIndex(
88-
heading => heading.line < item.line
89-
&& heading.level < item.level
141+
heading =>
142+
heading.type == SuggestionType.Heading
143+
&& (heading as HeadingSuggestion).line < item.line
144+
&& (heading as HeadingSuggestion).level < item.level
90145
);
91146
if(parentIndex < 0) break;
92147
const parent = this.items[parentIndex];
148+
if(parent.type != SuggestionType.Heading) break;
93149

94150
if(string.length > 0) {
95-
string = `${parent.heading} > ${string}`;
151+
string = `${(parent as HeadingSuggestion).heading} > ${string}`;
96152
} else {
97-
string = parent.heading;
153+
string = (parent as HeadingSuggestion).heading;
98154
}
99-
item = parent;
155+
item = parent as HeadingSuggestion;
100156
}
101157

102158
return string;

src/main.ts

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { Editor, Notice, MarkdownView, Plugin, EditorRange, livePreviewState } from "obsidian";
2-
import { goAndScrollToLine, headingsForActiveFile } from "./utility";
3-
import { HeadingModal } from "./heading_modal";
1+
import { Editor, Notice, MarkdownView, Plugin, TAbstractFile } from "obsidian";
2+
import { HeadingModal, HeadingSuggestion, FileSuggestion, Suggestion, SuggestionType } from "./heading_modal";
43
import { GotoHeadingSettingTab } from "./settings_tab";
5-
import { DEFAULT_SETTINGS, GotoHeadingSettings } from "./settings";
4+
import { DEFAULT_SETTINGS, GotoHeadingSettings, FolderSortMethod, FolderSortMethodIdentifiers } from "./settings";
5+
import { fileNameWithoutPathOrExtension, goAndScrollToLine, headingsForActiveFile, headingsForActiveFolder, HeadingFolderChildrenFn } from "./utility";
6+
7+
const FolderSortMethodFn: { [method in FolderSortMethod]: HeadingFolderChildrenFn } = {
8+
[FolderSortMethod.ByNameAscending]: (children) => children.sort((a, b) => a.name.localeCompare(b.name)),
9+
[FolderSortMethod.ByNameDescending]: (children) => children.sort((a, b) => b.name.localeCompare(a.name)),
10+
[FolderSortMethod.ByModificationDateDescending]: (children) => children.sort((a, b) => b.stat.mtime - a.stat.mtime),
11+
[FolderSortMethod.ByModificationDateAscending]: (children) => children.sort((a, b) => a.stat.mtime - b.stat.mtime),
12+
[FolderSortMethod.ByCreationDateDescending]: (children) => children.sort((a, b) => b.stat.ctime - a.stat.ctime),
13+
[FolderSortMethod.ByCreationDateAscending]: (children) => children.sort((a, b) => a.stat.ctime - b.stat.ctime),
14+
};
615

716
export default class GotoHeadingPlugin extends Plugin {
817
settings: GotoHeadingSettings;
@@ -33,6 +42,14 @@ export default class GotoHeadingPlugin extends Plugin {
3342
this.openHeadingSwitcher(editor, view);
3443
},
3544
});
45+
46+
this.addCommand({
47+
id: "gotoheading-switcher-folder",
48+
name: "In folder: Open switcher",
49+
editorCallback: (editor: Editor, view: MarkdownView) => {
50+
this.openHeadingSwitcherInFolder(editor, view);
51+
},
52+
});
3653
}
3754

3855
async loadSettings() {
@@ -81,13 +98,15 @@ export default class GotoHeadingPlugin extends Plugin {
8198
modal.setPlaceholder("Go to heading...");
8299

83100
modal.items = headings.map(heading => ({
101+
type: SuggestionType.Heading,
102+
text: heading.heading,
84103
heading: heading.heading,
85104
level: heading.level,
86105
line: heading.position.start.line
87106
}));
88107

89108
modal.onChoose = (item) => {
90-
goAndScrollToLine(editor, item.line);
109+
goAndScrollToLine(editor, (item as HeadingSuggestion).line);
91110
modal.close();
92111
};
93112

@@ -98,4 +117,76 @@ export default class GotoHeadingPlugin extends Plugin {
98117

99118
modal.open();
100119
}
120+
121+
protected openHeadingSwitcherInFolder(editor: Editor, view: MarkdownView) {
122+
let sortMethodFn: HeadingFolderChildrenFn = (x => x);
123+
const sortMethodId = this.settings.fileSortOrder;
124+
if(FolderSortMethodIdentifiers.contains(sortMethodId)) {
125+
sortMethodFn = FolderSortMethodFn[FolderSortMethod[sortMethodId]];
126+
}
127+
const headings = headingsForActiveFolder(this.app, sortMethodFn);
128+
if(!headings.length) return;
129+
130+
let modal = new HeadingModal(this.app);
131+
modal.setInstructions([
132+
{ command: "↑↓", purpose: "to navigate" },
133+
{ command: "↵", purpose: "to jump to file/heading" },
134+
{ command: "esc", purpose: "to dismiss" },
135+
]);
136+
modal.setPlaceholder("Go to file/heading...");
137+
138+
let items: Suggestion[] = [];
139+
for(const heading of headings) {
140+
const f: FileSuggestion = {
141+
type: SuggestionType.File,
142+
text: fileNameWithoutPathOrExtension(heading.file.path),
143+
path: heading.file.path
144+
};
145+
146+
items.push(f, ...heading.headings.map(heading => ({
147+
type: SuggestionType.Heading,
148+
text: heading.heading,
149+
heading: heading.heading,
150+
level: heading.level,
151+
line: heading.position.start.line,
152+
file: f
153+
})));
154+
}
155+
modal.items = items;
156+
157+
modal.onChoose = async (item) => {
158+
switch(item.type) {
159+
case SuggestionType.File: {
160+
const s = item as FileSuggestion;
161+
await this.app.workspace.openLinkText(s.path, '', false);
162+
break;
163+
}
164+
case SuggestionType.Heading: {
165+
const s = item as HeadingSuggestion;
166+
if(s.file) {
167+
await this.app.workspace.openLinkText(s.file.path, '', false);
168+
}
169+
goAndScrollToLine(editor, s.line);
170+
break;
171+
}
172+
}
173+
modal.close();
174+
};
175+
176+
// Get the index of the nearest heading to the cursor
177+
const line = editor.getCursor().line;
178+
const activeFilePath = this.app.workspace.getActiveFile()?.path;
179+
let idx = items.findLastIndex(item =>
180+
item.type == SuggestionType.Heading
181+
&& (item as HeadingSuggestion).file?.path === activeFilePath
182+
&& (item as HeadingSuggestion).line <= line
183+
);
184+
if(idx < 0) {
185+
idx = items.findIndex(item => item.type == SuggestionType.File && (item as FileSuggestion).path === activeFilePath);
186+
}
187+
modal.defaultItemIndex = idx;
188+
modal.settings = this.settings;
189+
190+
modal.open();
191+
}
101192
}

src/settings.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1+
export enum FolderSortMethod {
2+
ByNameAscending,
3+
ByNameDescending,
4+
ByModificationDateDescending,
5+
ByModificationDateAscending,
6+
ByCreationDateDescending,
7+
ByCreationDateAscending,
8+
};
9+
export type FolderSortMethodId = keyof typeof FolderSortMethod;
10+
export const FolderSortMethodIdentifiers = Object.keys(FolderSortMethod).filter(x => isNaN(Number(x)));
11+
112
export interface GotoHeadingSettings {
213
highlightCurrentHeading: boolean;
14+
fileSortOrder: FolderSortMethodId;
315
}
416

517
export const DEFAULT_SETTINGS: GotoHeadingSettings = {
6-
highlightCurrentHeading: true
7-
};
18+
highlightCurrentHeading: true,
19+
fileSortOrder: FolderSortMethod[FolderSortMethod.ByNameAscending] as FolderSortMethodId,
20+
};

src/settings_tab.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { App, PluginSettingTab, Setting } from "obsidian";
1+
import { App, PluginSettingTab, Setting, Notice } from "obsidian";
22
import GotoHeadingPlugin from "./main";
3+
import { FolderSortMethod, FolderSortMethodId, FolderSortMethodIdentifiers } from "./settings";
4+
5+
const FolderSortMethodName: {[method in FolderSortMethod]: string} = {
6+
[FolderSortMethod.ByNameAscending]: "File name (A to Z)",
7+
[FolderSortMethod.ByNameDescending]: "File name (Z to A)",
8+
[FolderSortMethod.ByModificationDateDescending]: "Modified time (new to old)",
9+
[FolderSortMethod.ByModificationDateAscending]: "Modified time (old to new)",
10+
[FolderSortMethod.ByCreationDateDescending]: "Creation date (new to old)",
11+
[FolderSortMethod.ByCreationDateAscending]: "Creation date (old to new)",
12+
};
313

414
export class GotoHeadingSettingTab extends PluginSettingTab {
515
plugin: GotoHeadingPlugin;
@@ -25,5 +35,22 @@ export class GotoHeadingSettingTab extends PluginSettingTab {
2535
await this.plugin.saveSettings();
2636
});
2737
});
38+
39+
new Setting(containerEl)
40+
.setName("File sort order")
41+
.setDesc("How to sort files in the 'In folder' switcher")
42+
.addDropdown((dropdown) => {
43+
const ids = Object.keys(FolderSortMethodName).forEach((key, index) => {
44+
dropdown.addOption(FolderSortMethod[index], FolderSortMethodName[index as FolderSortMethod]);
45+
});
46+
dropdown
47+
.setValue(this.plugin.settings.fileSortOrder)
48+
.onChange(async (value) => {
49+
if(!FolderSortMethodIdentifiers.contains(value)) return;
50+
51+
this.plugin.settings.fileSortOrder = value as FolderSortMethodId;
52+
await this.plugin.saveSettings();
53+
});
54+
});
2855
}
2956
}

0 commit comments

Comments
 (0)