Skip to content

Commit

Permalink
F2 to rename, Shift+F2 to move
Browse files Browse the repository at this point in the history
  • Loading branch information
pjeby committed Aug 30, 2021
1 parent f15e099 commit 7dab54a
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 84 deletions.
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
## Quick Explorer for Obsidian

[Obsidian](https://obsidian.md)'s in-app file explorer is pretty flexible, but sometimes you only want to see the "current" folder, or a parent of it... *without* needing to open a sidebar, then close it again afterwards.
[Obsidian](https://obsidian.md)'s in-app file explorer is pretty flexible, but it's almost 100% mouse-driven with almost no keyboard navigation. Worse, if you have a lot of folders and lots of files in them, you end up spending a lot of time expanding and collapsing folders, and scrolling to find what you're looking for. This can be especially annoying when all you want is to do something with the "current" folder, or a parent of it... *without* needing to open a sidebar and close it again afterwards. And last, but not least, trying to rapidly preview the contents of a lot of notes with the mouse is a giant PITA.

This plugin fixes that problem by providing a "breadcrumbs bar" similar to the location bar dropdowns in Windows Explorer. The breadcrumbs appear in the application title bar, and you can click on any of them to get a dropdown with all of the files and folders in the same location as the breadcrumb item. You can then do almost anything you can do with the full file explorer:
Enter Quick Explorer. It's menu-based and keyboard-friendly, stays out of your way when you aren't using it, and makes it super-easy to navigate from either the vault root or current folder, without needing to scroll through or collapse a zillion other folders to find what you're looking for. You can even search by name within a folder, just by typing. There's an auto-preview feature that makes previewing lots of notes super easy, with no mousing and no popups overhanging the file list. And you can even see the path of the current file as a "breadcrumbs bar" in the window title bar!

Each breadcrumb, when clicked, drops down a list of the the files and folders in the same directory. So if you click on the breadcrumb for the current file, you'll see the items in its folder, and the first breadcrumb will show items in the vault root. No matter where you click, though, you can then do almost anything that can be done with Obsidian's built-in file explorer:

* Ctrl/Cmd + Hover to preview files (if the built-in Page Preview plugin is enabled)
* Click to open files (with ctrl or cmd to open in a new pane)
* Right-click to get a full context menu for any file, folder, or breadcrumb
* Drag any file, folder, or breadcrumb to drop anywhere that supports dropping (e.g. to stars, into text editors to create links, pane headers to open in the pane, folders in the file explorer to move them, Kanban lanes, etc.)

Like the built-in file explorer, Quick Explorer will either show all files, or only the ones supported by Obsidian, depending upon whether "Detect all file extensions" is enabled in the "Files and Links" options tab.

Quick explorer also includes two hotkeyable commands:

* **Browse vault**, which opens a menu for the vault root, and
* **Browse current folder**, which opens a menu for the active file's containing folder

With Obsidian 0.12.12 and above, keyboard navigation is also supported within the menus:
And an extensive set of keyboard operations is available as well:

* Typing normal text searches item names within the folder and selects the next matching folder or file
* Up, Down, Home, and End move within a folder
* Typing normal text searches item names within a folder (or context menu), selecting the next matching item
* Up, Down, Home, and End move within a folder or context menu
* Left and Right arrows select parent or child folders
* Enter selects an item to open, Ctrl-or-Cmd + Enter opens a file in a new pane
* Alt + Enter opens a context menu for the selected file or folder
* F2 initiates a rename of the current file or folder, Shift+F2 begins a move
* Tab toggles "quick preview" mode: when active, hovering or arrowing to an item will automatically display a hover preview for it, positioned so that it's always *outside* the menu (unless you're so deep in subfolders you've reached the edge of your screen). This makes it really easy to browse the contents of a folder just by arrowing down through it.

And speaking of previews, Quick Explorer's previews support **folder notes**! When hover-previewing a folder (or after arrowing to it in quick preview mode), it's checked for a note whose name is the same as the folder, and then that note is shown without you needing to open the folder first. It's a huge time saver if you have a lot of folder notes. (Check out the Note Folder Autorename plugin if you'd like to automatically rename or move folders when the note is renamed, too.)

Like the built-in file explorer, Quick Explorer will either show all files, or only the ones supported by Obsidian, depending upon whether "Detect all file extensions" is enabled in the "Files and Links" options tab.

Quick explorer also includes two hotkeyable commands:

* **Browse vault**, which opens the dropdown for the vault root, and
* **Browse current folder**, which opens the dropdown for the active file's containing folder

### Installation

Expand Down
108 changes: 75 additions & 33 deletions main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "quick-explorer",
"name": "Quick Explorer",
"version": "0.0.6",
"version": "0.0.7",
"description": "Perform file explorer operations (and see your current file path) from the title bar",
"minAppVersion": "0.12.12",
"isDesktopOnly": true
Expand Down
20 changes: 7 additions & 13 deletions src/ContextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Keymap, Notice, TAbstractFile, TFile, TFolder, View } from "obsidian";
import { Keymap, Modal, Notice, TAbstractFile, TFile, TFolder, View } from "obsidian";
import { PopupMenu, MenuParent } from "./menus";
import {i18n} from "i18next";

Expand All @@ -15,6 +15,9 @@ declare module "obsidian" {
enabled: boolean
instance: {
revealInFolder(file: TAbstractFile): void
moveFileModal: Modal & {
setCurrentFile(file: TAbstractFile): void
}
}
}
}
Expand All @@ -23,7 +26,7 @@ declare module "obsidian" {
interface FileManager {
promptForFolderDeletion(folder: TFolder): void
promptForFileDeletion(file: TFile): void
promptForFileRename(file: TFile): void
promptForFileRename(file: TAbstractFile): void
createNewMarkdownFile(parentFolder?: TFolder, pattern?: string): Promise<TFile>
}
}
Expand Down Expand Up @@ -54,7 +57,7 @@ export class ContextMenu extends PopupMenu {
if (haveFileExplorer) {
this.withExplorer(file)?.createAbstractFile("folder", file);
} else {
new Notice("The File Explorer core plugin must be enabled to rename folders")
new Notice("The File Explorer core plugin must be enabled to create new folders")
event.stopPropagation();
}
}));
Expand All @@ -64,17 +67,8 @@ export class ContextMenu extends PopupMenu {
this.addSeparator();
}
this.addItem(i => {
// Can't rename folder without file explorer
i.setDisabled(file instanceof TFolder && !haveFileExplorer);
i.setTitle(optName("rename")).setIcon("pencil").onClick(event => {
if (file instanceof TFile) {
this.app.fileManager.promptForFileRename(file);
} else if (haveFileExplorer) {
this.withExplorer(file)?.startRenameFile(file);
} else {
new Notice("The File Explorer core plugin must be enabled to rename folders")
event.stopPropagation();
}
this.app.fileManager.promptForFileRename(file);
});
});
this.addItem(i => i.setTitle(optName("delete")).setIcon("trash").onClick(() => {
Expand Down
97 changes: 74 additions & 23 deletions src/FolderMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TAbstractFile, TFile, TFolder, Keymap, Notice, App, Menu, HoverParent, debounce } from "obsidian";
import { TAbstractFile, TFile, TFolder, Keymap, Notice, App, Menu, HoverParent, debounce, MenuItem } from "obsidian";
import { hoverSource, startDrag } from "./Explorer";
import { PopupMenu, MenuParent } from "./menus";
import { ContextMenu } from "./ContextMenu";
Expand Down Expand Up @@ -54,10 +54,12 @@ export class FolderMenu extends PopupMenu {

constructor(public parent: MenuParent, public folder: TFolder, public selectedFile?: TAbstractFile, public opener?: HTMLElement) {
super(parent);
this.loadFiles(folder);
this.scope.register([], "Tab", this.togglePreviewMode.bind(this));
this.scope.register(["Mod"], "Enter", this.onEnter.bind(this));
this.scope.register(["Alt"], "Enter", this.onEnter.bind(this));
this.loadFiles(folder, selectedFile);
this.scope.register([], "Tab", this.togglePreviewMode.bind(this));
this.scope.register(["Mod"], "Enter", this.onEnter.bind(this));
this.scope.register(["Alt"], "Enter", this.onEnter.bind(this));
this.scope.register([], "F2", this.doRename.bind(this));
this.scope.register(["Shift"], "F2", this.doMove.bind(this));

const { dom } = this;
dom.style.setProperty(
Expand All @@ -82,6 +84,39 @@ export class FolderMenu extends PopupMenu {
return super.onArrowLeft() ?? this.openBreadcrumb(this.opener?.previousElementSibling);
}

doRename() {
const file = this.currentFile()
if (file) this.app.fileManager.promptForFileRename(file);
}

doMove() {
const explorerPlugin = this.app.internalPlugins.plugins["file-explorer"];
if (!explorerPlugin.enabled) {
new Notice("File explorer core plugin must be enabled to move files or folders");
return;
}
const modal = explorerPlugin.instance.moveFileModal;
modal.setCurrentFile(this.currentFile());
modal.open()
}

currentItem() {
return this.items[this.selected];
}

currentFile() {
return this.fileForDom(this.currentItem()?.dom)
}

fileForDom(targetEl: HTMLDivElement) {
const { filePath } = targetEl?.dataset;
if (filePath) return this.app.vault.getAbstractFileByPath(filePath);
}

itemForPath(filePath: string) {
return this.items.findIndex(i => i.dom.dataset.filePath === filePath);
}

openBreadcrumb(element: Element) {
if (element && this.rootMenu() === this) {
const prevExplorable = this.opener.previousElementSibling;
Expand All @@ -92,17 +127,16 @@ export class FolderMenu extends PopupMenu {
}

onArrowRight(): boolean | undefined {
const targetEl = this.items[this.selected]?.dom;
const { filePath } = targetEl?.dataset;
const file = filePath && this.app.vault.getAbstractFileByPath(filePath);
const file = this.currentFile();
if (file instanceof TFolder && file !== this.selectedFile) {
this.onClickFile(file, targetEl);
this.onClickFile(file, this.currentItem().dom);
return false;
}
return this.openBreadcrumb(this.opener?.nextElementSibling);
}

loadFiles(folder: TFolder) {
loadFiles(folder: TFolder, selectedFile?: TAbstractFile) {
this.dom.empty(); this.items = [];
const allFiles = this.app.vault.getConfig("showUnsupportedFiles");
const {children, parent} = folder;
const items = children.slice().sort((a: TAbstractFile, b: TAbstractFile) => alphaSort(a.name, b.name))
Expand All @@ -114,6 +148,7 @@ export class FolderMenu extends PopupMenu {
folders.map(this.addFile, this);
if (folders.length && files.length) this.addSeparator();
files.map( this.addFile, this);
if (selectedFile) this.select(this.itemForPath(selectedFile.path)); else this.selected = -1;
}

addFile(file: TAbstractFile) {
Expand All @@ -128,9 +163,6 @@ export class FolderMenu extends PopupMenu {
if (file.extension !== "md") i.dom.createDiv({text: file.extension, cls: "nav-file-tag"});
}
i.onClick(e => this.onClickFile(file, i.dom, e))
if (file === this.selectedFile) {
this.select(this.items.length-1);
}
});
}

Expand All @@ -140,10 +172,31 @@ export class FolderMenu extends PopupMenu {

onload() {
super.onload();
this.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
if (this.folder === file.parent) {
// Destination was here; refresh the list
const selectedFile = this.itemForPath(oldPath) >= 0 ? file : this.currentFile();
this.loadFiles(this.folder, selectedFile);
} else {
// Remove it if it was moved out of here
this.removeItemForPath(oldPath);
}
}));
this.registerEvent(this.app.vault.on("delete", file => this.removeItemForPath(file.path)));

// Activate preview immediately if applicable
if (autoPreview && this.selected != -1) this.showPopover();
}

removeItemForPath(path: string) {
const posn = this.itemForPath(path);
if (posn < 0) return;
const item = this.items[posn];
if (this.selected > posn) this.selected -= 1;
item.dom.detach()
this.items.remove(item);
}

hide() {
this.hidePopover();
return super.hide();
Expand All @@ -169,7 +222,7 @@ export class FolderMenu extends PopupMenu {
showPopover = debounce(() => {
this.hidePopover();
if (!autoPreview) return;
this.maybeHover(this.items[this.selected]?.dom, file => this.app.workspace.trigger('link-hover', this, null, file.path, ""));
this.maybeHover(this.currentItem()?.dom, file => this.app.workspace.trigger('link-hover', this, null, file.path, ""));
}, 50, true)

onItemHover = (event: MouseEvent, targetEl: HTMLDivElement) => {
Expand All @@ -180,10 +233,9 @@ export class FolderMenu extends PopupMenu {

maybeHover(targetEl: HTMLDivElement, cb: (file: TFile) => void) {
if (!this.canShowPopover()) return;
const { filePath } = targetEl?.dataset;
let file = filePath && this.app.vault.getAbstractFileByPath(filePath);
let file = this.fileForDom(targetEl)
if (file instanceof TFolder) {
file = this.app.vault.getAbstractFileByPath(filePath+"/"+file.name+".md");
file = this.app.vault.getAbstractFileByPath(file.path+"/"+file.name+".md");
}
if (file instanceof TFile && previewIcons[this.app.viewRegistry.getTypeByExtension(file.extension)]) {
cb(file)
Expand All @@ -198,15 +250,15 @@ export class FolderMenu extends PopupMenu {
set hoverPopover(popover) {
if (popover && !this.canShowPopover()) { popover.hide(); return; }
this._popover = popover;
if (autoPreview && popover) {
if (autoPreview && popover && this.currentItem()) {
// Position the popover so it doesn't overlap the menu horizontally (as long as it fits)
// and so that its vertical position overlaps the selected menu item (placing the top a
// bit above the current item, unless it would go off the bottom of the screen)
const hoverEl = popover.hoverEl;
hoverEl.show();
const
menu = this.dom.getBoundingClientRect(),
selected = this.items[this.selected].dom.getBoundingClientRect(),
selected = this.currentItem().dom.getBoundingClientRect(),
container = hoverEl.offsetParent || document.documentElement,
popupHeight = hoverEl.offsetHeight,
left = Math.min(menu.right + 2, container.clientWidth - hoverEl.offsetWidth),
Expand All @@ -218,8 +270,7 @@ export class FolderMenu extends PopupMenu {
}

onItemClick = (event: MouseEvent, target: HTMLDivElement) => {
const { filePath } = target.dataset;
const file = this.app.vault.getAbstractFileByPath(filePath);
const file = this.fileForDom(target);
this.lastOver = target;
if (!file) return;
if (!this.onClickFile(file, target, event)) {
Expand All @@ -242,6 +293,7 @@ export class FolderMenu extends PopupMenu {
this.app.workspace.openLinkText(file.path, "", event && Keymap.isModifier(event, "Mod"));
// Close the entire menu tree
this.rootMenu().hide();
event?.stopPropagation();
return true;
} else {
new Notice(`.${file.extension} files cannot be opened in Obsidian; Use "Open in Default App" to open them externally`);
Expand All @@ -264,8 +316,7 @@ export class FolderMenu extends PopupMenu {
}

onItemMenu = (event: MouseEvent, target: HTMLDivElement) => {
const { filePath } = target.dataset;
const file = this.app.vault.getAbstractFileByPath(filePath);
const file = this.fileForDom(target);
if (file) {
this.lastOver = target;
new ContextMenu(this, file).cascade(target, event);
Expand Down
4 changes: 3 additions & 1 deletion src/quick-explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Plugin, TAbstractFile} from "obsidian";
import {Plugin, TAbstractFile, TFolder} from "obsidian";
import {mount, unmount} from "redom";
import {Explorer, hoverSource} from "./Explorer";

Expand Down Expand Up @@ -32,6 +32,8 @@ export default class extends Plugin {

this.addCommand({ id: "browse-vault", name: "Browse vault", callback: () => { this.explorer?.browseVault(); }, });
this.addCommand({ id: "browse-current", name: "Browse current folder", callback: () => { this.explorer?.browseCurrent(); }, });

Object.defineProperty(TFolder.prototype, "basename", {get(){ return this.name; }, configurable: true})
}

onunload() {
Expand Down
2 changes: 1 addition & 1 deletion versions.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"0.0.6": "0.12.12",
"0.0.7": "0.12.12",
"0.0.5": "0.12.10",
"0.0.1": "0.12.3"
}

0 comments on commit 7dab54a

Please sign in to comment.