diff --git a/src/folderSuggestModal.ts b/src/folderSuggestModal.ts index 190d05a..13268b3 100644 --- a/src/folderSuggestModal.ts +++ b/src/folderSuggestModal.ts @@ -5,6 +5,7 @@ import { WorkspaceLeaf, View, FuzzyMatch, + Notice, } from "obsidian"; import type FolderNavigatorPlugin from "./main"; import { FolderDisplayMode } from "./settings"; @@ -53,6 +54,11 @@ interface FolderWithScore { match: FuzzyMatch; } +interface CreateFolderOption extends TFolder { + _isCreateOption: boolean; + _folderName: string; +} + export class FolderSuggestModal extends FuzzySuggestModal { folders: TFolder[]; plugin: FolderNavigatorPlugin; @@ -439,6 +445,54 @@ export class FolderSuggestModal extends FuzzySuggestModal { this.plugin, ); + // If query is provided, add "Create new folder" option at the end + if (normalizedQuery.length > 0) { + // Filter out separators to count real folder matches + const realFolderMatches = limitedResults.filter( + (result) => !(result.item as any)._isSeparator, + ); + + // Only show create option if the exact folder doesn't already exist + const exactMatch = realFolderMatches.some( + (result) => + result.item.path.toLowerCase() === normalizedQuery || + result.item.name.toLowerCase() === normalizedQuery, + ); + + if (!exactMatch) { + const createOption = {} as CreateFolderOption; + createOption._isCreateOption = true; + createOption._folderName = query.trim(); + + // Add separator before create option if there are results + if (limitedResults.length > 0) { + const separator = {} as TFolder; + (separator as any)._isSeparator = true; + (separator as any)._separatorText = "—————"; + + return [ + ...limitedResults, + { + item: separator, + match: { score: 0, matches: [] }, + }, + { + item: createOption as TFolder, + match: { score: 0, matches: [] }, + }, + ]; + } else { + // No results, just show create option + return [ + { + item: createOption as TFolder, + match: { score: 0, matches: [] }, + }, + ]; + } + } + } + return limitedResults; } @@ -451,6 +505,10 @@ export class FolderSuggestModal extends FuzzySuggestModal { if ((folder as any)._isSeparator) { return (folder as any)._separatorText; } + // For create folder option + if ((folder as CreateFolderOption)._isCreateOption) { + return `Create new folder: ${(folder as CreateFolderOption)._folderName}`; + } return folder.path; } @@ -516,26 +574,55 @@ export class FolderSuggestModal extends FuzzySuggestModal { } /** - * Event handler for when a folder is chosen from the suggestion list + * Handle creating a new folder */ - onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent): void { - // Check if this is a separator and ignore if so - if ((folder as any)._isSeparator) { - return; + private async handleCreateFolder(folderName: string): Promise { + try { + log(`Creating new folder: "${folderName}"`, this.plugin); + + // Get the base path from settings + const basePath = this.plugin.settings.newFolderLocation; + const fullPath = basePath + ? `${basePath}/${folderName}` + : folderName; + + log(`Full path for new folder: "${fullPath}"`, this.plugin); + + // Check if folder already exists + const existingFolder = + this.app.vault.getAbstractFileByPath(fullPath); + if (existingFolder) { + new Notice(`Folder "${fullPath}" already exists`); + return; + } + + // Create the folder + const newFolder = await this.app.vault.createFolder(fullPath); + log(`Folder created successfully: "${fullPath}"`, this.plugin); + + new Notice(`Folder "${fullPath}" created successfully`); + + // Close the modal + this.close(); + + // Navigate to the newly created folder + setTimeout(() => { + this.navigateToFolder(newFolder); + }, 100); + } catch (error) { + logError(`Error creating folder: ${error}`); + new Notice(`Failed to create folder: ${error.message || error}`); } + } - log( - `Folder selected: "${folder.path}" (name: "${folder.name}")`, - this.plugin, - ); - + /** + * Navigate to a folder (same logic as in onChooseItem) + */ + private navigateToFolder(folder: TFolder): void { try { // Update folder history this.updateFolderHistory(folder); - // Close the modal first - this.close(); - // Make sure the file explorer is visible const fileExplorerLeaves = this.app.workspace.getLeavesOfType("file-explorer"); @@ -609,6 +696,35 @@ export class FolderSuggestModal extends FuzzySuggestModal { } } + /** + * Event handler for when a folder is chosen from the suggestion list + */ + onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent): void { + // Check if this is a separator and ignore if so + if ((folder as any)._isSeparator) { + return; + } + + // Check if this is a create folder option + if ((folder as CreateFolderOption)._isCreateOption) { + this.handleCreateFolder((folder as CreateFolderOption)._folderName); + return; + } + + log( + `Folder selected: "${folder.path}" (name: "${folder.name}")`, + this.plugin, + ); + + // Close the modal first + this.close(); + + // Navigate to the selected folder + setTimeout(() => { + this.navigateToFolder(folder); + }, 100); + } + /** * Expand a folder by finding and clicking its title directly in the DOM */ @@ -747,6 +863,25 @@ export class FolderSuggestModal extends FuzzySuggestModal { return; } + // Check if this is a create folder option + if ((item.item as CreateFolderOption)._isCreateOption) { + el.empty(); + const createEl = el.createDiv({ + cls: "folder-create-option", + }); + const iconEl = createEl.createSpan({ + cls: "folder-create-icon", + }); + iconEl.setText("➕ "); + const textEl = createEl.createSpan({ + cls: "folder-create-text", + }); + textEl.setText( + `Create new folder: "${(item.item as CreateFolderOption)._folderName}"`, + ); + return; + } + // Custom rendering with keyword highlighting el.empty(); const folder = item.item; diff --git a/src/settings.ts b/src/settings.ts index 0178880..98ac5c3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -20,6 +20,7 @@ export interface PluginSettings { accessCount: number; // visit count } >; + newFolderLocation: string; // Path where new folders should be created (empty string = root) } export const DEFAULT_SETTINGS: PluginSettings = { @@ -30,4 +31,5 @@ export const DEFAULT_SETTINGS: PluginSettings = { recentFoldersToShow: 5, frequentFoldersToShow: 5, folderHistory: {}, + newFolderLocation: "", // Empty string means root folder }; diff --git a/src/settingsTab.ts b/src/settingsTab.ts index 627e0a7..bf47642 100644 --- a/src/settingsTab.ts +++ b/src/settingsTab.ts @@ -5,10 +5,43 @@ import { DropdownComponent, Modal, Notice, + FuzzySuggestModal, + TFolder, } from "obsidian"; import FolderNavigatorPlugin from "./main"; import { FolderDisplayMode } from "./settings"; +/** + * Modal for searching and selecting a folder for new folder creation location + */ +class FolderSelectionModal extends FuzzySuggestModal { + plugin: FolderNavigatorPlugin; + onSelect: (folder: TFolder) => void; + + constructor( + app: App, + plugin: FolderNavigatorPlugin, + onSelect: (folder: TFolder) => void, + ) { + super(app); + this.plugin = plugin; + this.onSelect = onSelect; + this.setPlaceholder("Search for a folder..."); + } + + getItems(): TFolder[] { + return this.app.vault.getAllFolders(); + } + + getItemText(folder: TFolder): string { + return folder.path || "/"; + } + + onChooseItem(folder: TFolder): void { + this.onSelect(folder); + } +} + export class SettingsTab extends PluginSettingTab { plugin: FolderNavigatorPlugin; @@ -89,6 +122,43 @@ export class SettingsTab extends PluginSettingTab { }), ); + // New folder creation section + new Setting(containerEl).setName("Folder creation").setHeading(); + + new Setting(containerEl) + .setName("Default location for new folders") + .setDesc( + "Choose where new folders will be created when using the 'Create folder' option. Default is the vault root.", + ) + .addButton((button) => { + const displayPath = + this.plugin.settings.newFolderLocation || "/ (Root)"; + button.setButtonText(displayPath).onClick(() => { + const modal = new FolderSelectionModal( + this.app, + this.plugin, + async (folder: TFolder) => { + this.plugin.settings.newFolderLocation = + folder.path; + await this.plugin.saveSettings(); + button.setButtonText(folder.path || "/ (Root)"); + }, + ); + modal.open(); + }); + }) + .addExtraButton((button) => { + button + .setIcon("reset") + .setTooltip("Reset to root folder") + .onClick(async () => { + this.plugin.settings.newFolderLocation = ""; + await this.plugin.saveSettings(); + // Update button text by recreating the setting + this.display(); + }); + }); + // Folder display preferences section new Setting(containerEl) .setName("Folder display preferences") diff --git a/styles.css b/styles.css index 0c5a200..57d1fce 100644 --- a/styles.css +++ b/styles.css @@ -18,3 +18,19 @@ .suggestion-highlight { font-weight: bold; } + +.folder-create-option { + display: flex; + align-items: center; + padding: 4px 0; + color: var(--text-accent); +} + +.folder-create-icon { + margin-right: 6px; + font-size: 1.1em; +} + +.folder-create-text { + font-style: italic; +}