-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.ts
283 lines (237 loc) · 9.13 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import { Plugin, TFile, TFolder, WorkspaceLeaf } from 'obsidian';
const enum Direction {
Forward,
Backwards
}
export interface FileExplorerItem {
file: TFile | TFolder;
collapsed?: boolean;
titleEl: Element;
setCollapsed?: (state: boolean) => void;
}
export default class FileExplorerKeyboardNav extends Plugin {
currentFilelessFolder : TFolder = null;
async onload() {
this.addCommand({
id: 'file-explorer-next-file',
name: 'Go to next file',
callback: () => {
this.openNextFile(Direction.Forward);
}
});
this.addCommand({
id: 'file-explorer-previous-file',
name: 'Go to previous file',
callback: () => {
this.openNextFile(Direction.Backwards);
}
});
this.addCommand({
id: 'file-explorer-next-folder',
name: 'Go to next folder',
callback: () => {
this.nextFolder(Direction.Forward);
}
});
this.addCommand({
id: 'file-explorer-previous-folder',
name: 'Go to previous folder',
callback: () => {
this.nextFolder(Direction.Backwards);
}
});
}
// Open the file after the currently open file; if no such file exists open the first file of the folder
openNextFile(direction: Direction = Direction.Forward) : Promise<void> {
const openFile = app.workspace.getActiveFile();
// if we have no file, we're starting at the root and just grabbing the first file
if (!openFile) {
this.openNextFolder(app.vault.getRoot(), direction);
} else {
const currentFolder = this.getCurrentFolder();
const files = fileExplorerSortFiles(currentFolder);
let openNextFile = false;
// set indices of for loop
let i = (direction === Direction.Forward) ? 0 : files.length - 1;
const stop = (direction === Direction.Forward) ? files.length : -1;
const step = (direction === Direction.Forward) ? 1 : -1
// loop forwards/backward, find the currently open file, and after finding it open next
for (; i != stop; i += step) {
if (!openNextFile && files[i] === openFile) {
openNextFile = true;
} else if (openNextFile) {
app.workspace.activeLeaf.openFile(files[i]);
this.scroll(files[i]);
return;
}
}
}
}
nextFolder(direction : Direction = Direction.Forward) {
let openFolder;
// if we are "in" a folder different from the one we have a file open for
if (this.currentFilelessFolder) {
openFolder = this.currentFilelessFolder;
this.currentFilelessFolder = null;
} else {
openFolder = this.getCurrentFolder();
}
// if we have no file, we're starting at the root and just grabbing the first file
if (openFolder && !openFolder.isRoot()) {
const parentFolder = openFolder.parent;
const siblingFolders = fileExplorerSortFolders(parentFolder);
let openNextFolder = false;
// set indices of for loop
let i = (direction === Direction.Forward) ? 0 : siblingFolders.length - 1;
const stop = (direction === Direction.Forward) ? siblingFolders.length : -1;
const step = (direction === Direction.Forward) ? 1 : -1
// loop forwards/backward, find the currently open folder, and after finding it open first file of next
for (; i != stop; i += step) {
if (!openNextFolder && siblingFolders[i] === openFolder) {
openNextFolder = true;
} else if (openNextFolder && siblingFolders[i].children) { //only open if next folder has children
this.openNextFolder(siblingFolders[i]);
this.expandFolder(siblingFolders[i]);
return;
}
}
}
}
// open the first file of the given folder, or if folder doesn't have valid files store it as the current folder
// if direction is set to backwards, will open the last file
openNextFolder(folder : TFolder, direction: Direction = Direction.Forward) {
const files = fileExplorerSortFiles(folder);
if (files.length > 0) {
if (direction === Direction.Forward) {
app.workspace.activeLeaf.openFile(files[0]);
} else {
app.workspace.activeLeaf.openFile(files[files.length - 1]);
}
} else {
this.currentFilelessFolder = folder;
}
}
// return the parent folder of the active view, or, if such does not exist, null
getCurrentFolder() : TFolder {
const activeView = app.workspace.getActiveFile();
if (activeView) {
return activeView.parent;
}
}
/** Stolen from https://github.com/OfficerHalf/obsidian-collapse-all/blob/61f3460b6416fbc9e1fce0fd0043981a84a79bcd/src/provider/file-explorer.ts#L84=
* Get all `fileItems` on explorer view. This property is not documented.*/
getExplorerItems(leaf: WorkspaceLeaf): FileExplorerItem[] {
return Object.values((leaf.view as any).fileItems) as FileExplorerItem[];
}
// Get the FileExplorerItem of the passed file or folder
getFileExplorerItem(leaf: WorkspaceLeaf, abstrFile: TFolder | TFile): FileExplorerItem {
const allItems = this.getExplorerItems(leaf);
// This is a very naiive but cheap way to do this.
return allItems.filter(item =>
item.file.path === abstrFile.path
)[0];
}
//uncollapse the given folder and scroll to it if needed
expandFolder(folder : TFolder) {
const leaf = app.workspace.getLeavesOfType('file-explorer').first();
const folderItem = this.getFileExplorerItem(leaf, folder);
this.scroll(folderItem);
if (folderItem.collapsed) {
folderItem.setCollapsed(false);
}
}
// scroll to given item in file explorer
scroll(item : FileExplorerItem | TFile | TFolder) {
if ((item instanceof TFile) || (item instanceof TFolder)) { //written as an or instead of not to make TS happy
const leaf = app.workspace.getLeavesOfType('file-explorer').first();
item = this.getFileExplorerItem(leaf, item);
}
//@ts-ignore ; doesn't know about scrollIntoViewIfNeeded()
item.titleEl.scrollIntoViewIfNeeded({ behavior: "smooth", block: "nearest" });
}
}
// filter out unsupported files and folders, then sort the remaining children of the passed folder
// according to the order they are displayed in the file explorer
function fileExplorerSortFiles(folder : TFolder) : TFile[] {
const files = removeUnsupportedFilesAndFolders(folder);
const collator = new Intl.Collator(navigator.languages[0] || navigator.language,
{ numeric: true, ignorePunctuation: false, caseFirst: 'upper' });
// reverse alphabetical is still sorted alphabetically by extension if basenames match
const reverseCollator = new Intl.Collator(navigator.languages[0] || navigator.language,
{ numeric: true, ignorePunctuation: false, caseFirst: 'lower' });
//@ts-ignore
const sortOrder: string = app.vault.config.fileSortOrder;
// sort using localeSort(), but in alphabetical sort substrings go *before* the longer string
files.sort((a: TFile , b: TFile ) => {
if (sortOrder === 'alphabetical') {
if (a.basename.startsWith(b.basename) && a.basename !== b.basename) {
return 1;
} else if (b.basename.startsWith(a.basename) && a.basename !== b.basename) {
return -1;
} else {
return collator.compare(a.name, b.name);
}
} else if (sortOrder === 'alphabeticalReverse') {
if (a.basename.startsWith(b.basename) && a.basename !== b.basename) {
return -1;
} else if (b.basename.startsWith(a.basename) && a.basename !== b.basename) {
return 1;
} else if (a.basename === b.basename) {
return reverseCollator.compare(a.extension, b.extension);
} else {
return reverseCollator.compare(b.name, a.name);
}
} else if (sortOrder === 'byModifiedTime') {
return b.stat.mtime - a.stat.mtime;
} else if (sortOrder === 'byModifiedTimeReverse') {
return a.stat.mtime - b.stat.mtime;
} else if (sortOrder === 'byCreatedTime') {
return b.stat.ctime - a.stat.ctime;
} else if (sortOrder === 'byCreatedTimeReverse') {
return a.stat.ctime - b.stat.ctime;
} else {
throw new Error("Unsupported sort order.")
}
});
return files;
}
// sort folders in file explorer order
// note that regardless of the user-defined sort order these are always alphabetical
function fileExplorerSortFolders(folder : TFolder) : TFolder[] {
const childFolders = removeFiles(folder);
const collator = new Intl.Collator(navigator.languages[0] || navigator.language,
{ numeric: true, ignorePunctuation: false, caseFirst: 'upper' });
childFolders.sort((a: TFolder, b: TFolder) => {
if (a.name.startsWith(b.name) && a.name !== b.name) {
return 1;
} else if (b.name.startsWith(a.name) && a.name !== b.name) {
return -1;
} else {
return collator.compare(a.name, b.name);
}
});
return childFolders;
}
// Removes folders and unsupported file formats from the passed folder, returns a list of files
// https://help.obsidian.md/Advanced+topics/Accepted+file+formats
function removeUnsupportedFilesAndFolders(folder : TFolder) : TFile[] {
const extensionRegex = new RegExp(/^.*\.(md|jpg|png|jpg|jpeg|gif|bmp|svg|mp3|webm|wav|m4a|ogg|3gp|flac|mp4|webm|ogv|pdf|opus)$/i);
const filesAndFolders = folder.children;
const checked_files : TFile[] = [];
for (const abstrFile of filesAndFolders) {
if (abstrFile instanceof TFile && extensionRegex.test(abstrFile.name)) {
checked_files.push(abstrFile);
}
}
return checked_files;
}
// Removes all files from the passed folder, returns a list of folders
function removeFiles(folder : TFolder) : TFolder[] {
const folders = [];
for (const abstrFile of folder.children) {
if (abstrFile instanceof TFolder) {
folders.push(abstrFile);
}
}
return folders;
}