Compare commits

..

27 commits
0.2.0 ... main

Author SHA1 Message Date
wz
4175b9dc30 0.5.0 2025-10-07 21:26:57 +02:00
wz
28db3e9d50 refactor: reorganize settings tab layout and improve section headings 2025-10-07 21:21:37 +02:00
wz
d6f18b52c2 feat: add create new folder option in folder navigator modal 2025-10-07 21:15:30 +02:00
wz
95e5d2937f 0.4.1 2025-09-30 23:37:07 +02:00
wz
5780b3e8bc style: remove accent color from suggestion highlights 2025-09-30 23:36:47 +02:00
wz
b1af049004 0.4.0 2025-09-30 22:44:32 +02:00
wz
658adcbfe4 refactor: improve folder search by excluding redundant child matches instead of deprioritizing 2025-09-30 22:42:43 +02:00
wz
89df8eb024 feat: implement keyword highlighting in folder search suggestions 2025-09-30 22:26:39 +02:00
wz
d6de79d372 refactor: remove unused match position tracking in folder search results 2025-09-30 16:50:46 +02:00
wz
97974cc801 improve fuzzy search 2025-09-30 16:41:18 +02:00
wz
3b440f3222 Update README.md 2025-05-03 09:43:46 +02:00
wz
bf52a4b2a5 Improve code format 2025-04-30 20:40:30 +02:00
wz
78f03608ea 0.3.1 2025-04-30 20:40:05 +02:00
wz
1dd71601ce Refactor settings tab to dynamically show/hide folder history options based on mode 2025-04-30 20:39:49 +02:00
wz
71294a3b94 Rename FolderSortMode to FolderDisplayMode and update related UI text for clarity 2025-04-30 19:37:08 +02:00
wz
5929cd9cf2 Improve code format 2025-04-30 19:21:30 +02:00
wz
72506484ee 0.3.0 2025-04-30 19:21:11 +02:00
wz
99e1d036aa Add visual separators and confirmation modal for folder history reset 2025-04-30 19:20:45 +02:00
wz
eb222d3e52 Add sort options
Sort based on recency and frequency.
2025-04-30 19:00:44 +02:00
wz
5cfe391aec Add demo 2025-04-06 10:34:25 +02:00
wz
79dc260bb7 Improve code format 2025-04-06 10:25:53 +02:00
wz
b339221329 0.2.1 2025-04-06 10:25:33 +02:00
wz
1a4a369063 Clean up code 2025-04-06 10:24:44 +02:00
wz
fad131f902 Fix folder expansion 2025-04-06 10:18:01 +02:00
wz
4227778ba0 Update README.md 2025-04-05 11:58:07 +02:00
wz
c23b67f477 Update README.md 2025-04-05 11:52:24 +02:00
wz
f2ef0c2795 Improve code format 2025-04-05 11:42:22 +02:00
12 changed files with 1438 additions and 211 deletions

View file

@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.5.0] - 2025-10-07
### Changes
- refactor: reorganize settings tab layout and improve section headings
- feat: add create new folder option in folder navigator modal
## [0.4.1] - 2025-09-30
### Changes
- style: remove accent color from suggestion highlights
## [0.4.0] - 2025-09-30
### Changes
- refactor: improve folder search by excluding redundant child matches instead of deprioritizing
- feat: implement keyword highlighting in folder search suggestions
- refactor: remove unused match position tracking in folder search results
- improve fuzzy search
- Update README.md
- Improve code format
## [0.3.1] - 2025-04-30
### Changes
- Refactor settings tab to dynamically show/hide folder history options based on mode
- Rename FolderSortMode to FolderDisplayMode and update related UI text for clarity
- Improve code format
## [0.3.0] - 2025-04-30
### Changes
- Add visual separators and confirmation modal for folder history reset
- Add sort options
- Add demo
- Improve code format
## [0.2.1] - 2025-04-06
### Changes
- Clean up code
- Fix folder expansion
- Update README.md
- Update README.md
- Improve code format
## [0.2.0] - 2025-04-05
### Changes

View file

@ -4,39 +4,61 @@
An [Obsidian](https://obsidian.md) plugin that allows you to quickly navigate to folders in your vault using fuzzy search.
![demo](/docs/attachment/demo.gif)
## Features
- Quick folder navigation using fuzzy search
- Configurable maximum number of search results
- Works with nested folders
- Expand target folder on navigation
- Smart folder prioritization options:
- Default order (alphabetical)
- Recently visited folders displayed at the top
- Frequently visited folders displayed at the top
- Option to reset folder navigation history if needed
## Usage
1. Press the hotkey (default: Cmd/Ctrl+Shift+O) to open the folder search modal
2. Type to search for folders - the search is fuzzy, so you don't need to type the exact name
3. Use arrow keys to navigate through the results
4. Press Enter to select a folder and navigate to it in the file explorer
## Videos and Articles
## Settings
### Articles
- **Hotkey**: Customize the keyboard shortcut to open the folder navigator (requires restart)
- **Maximum results**: Set the maximum number of folders to show in search results (5-50)
- [The Strategy of Note Taking: Folders, Tags, Links, and Redundancy - PTKM](https://ptkm.net/blog-note-taking-strategy-folders-tags-links-redundancy)
## Installation
## Why Folder Navigator?
1. Open Settings in Obsidian
2. Navigate to Community Plugins and disable Safe Mode
3. Click Browse and search for "Folder Navigator"
4. Install the plugin
5. Enable the plugin in the Community Plugins tab
I really enjoy using Obsidian's file search feature, which allows us to quickly search for files and navigate to them. There are also third-party plugins that further enhance this experience.
## Manual Installation
However, I need to navigate to folders in a similar way. Unfortunately, I couldn't find a method to do this, either by using Obsidian's official features or third-party plugins. That's why I'm developing this plugin to achieve that. **It allows you to navigate to folders just as you can when navigating to files.**
1. Download the latest [release](https://github.com/wenlzhang/obsidian-folder-navigator/releases)
2. Extract the files into your vault's `.obsidian/plugins/obsidian-folder-navigator/` directory
3. Reload Obsidian
4. Enable the plugin in Settings > Community Plugins
## Documentation
## Support
📚 **[View Full Documentation](https://ptkm.net/obsidian-folder-navigator)**
<a href='https://ko-fi.com/C0C66C1TB' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
Visit the documentation site to learn how to make the most of Folder Navigator in your Obsidian workflow.
## Support & Community
This plugin is developed and maintained during my free time. A lot of thought, energy, and care goes into making it reliable and user-friendly.
If you find Folder Navigator valuable in your daily workflow:
- If it helps you organize your folders more effectively
- If it saves you time and mental energy navigating your knowledge base
Please consider supporting my work. Your support would help me dedicate more time and energy to:
- Developing new features
- Maintaining code quality
- Providing support and documentation
- Making the plugin even better for everyone
### Ways to Support
You can support this project in several ways:
- ⭐ Star the project on GitHub
- 💝 <a href='https://ko-fi.com/C0C66C1TB' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
- 💌 Share your success stories and feedback
- 📢 Spread the word about the plugin
- 🐛 [Report issues](https://github.com/wenlzhang/obsidian-folder-navigator/issues) to help improve the plugin
Thank you for being part of this journey! 🙏

BIN
docs/attachment/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

View file

@ -0,0 +1,169 @@
# Search Strategy Improvements
This document describes the improvements made to the folder search functionality to address issues #4 and #8.
## Issues Addressed
### Issue #4: Better Match Prioritization
**Problem**: When searching for "abc", the plugin would show "abc" along with all its subdirectories like "abc/xxx", "abc/yyy", etc., making it difficult to find the target folder.
**Solution**: Implemented intelligent parent-child deduplication that:
- Prioritizes parent folder matches over their children
- Completely excludes child folders unless they have additional keyword matches beyond what the parent has
- Only shows children when they contain additional matched keywords not found in the parent path
### Issue #8: Out-of-Order Keyword Matching
**Problem**: Search only worked when keywords were in the correct order. For example, searching "bar foo" would not match "foo/bar/baz".
**Solution**: Implemented keyword-based matching that:
- Splits the search query into space-separated keywords
- Matches keywords independently in any order
- Requires all keywords to be present in the folder path for a match
## Implementation Details
### Custom `getSuggestions` Method
The plugin now uses a custom `getSuggestions` method that overrides the default fuzzy search with a more sophisticated scoring system:
#### 1. Keyword Extraction
```typescript
const keywords = normalizedQuery.split(/\s+/).filter((k) => k.length > 0);
```
- Splits query by whitespace
- Filters out empty strings
- Allows matching keywords in any order
#### 2. Scoring System
Each folder is scored based on multiple factors:
**Keyword Match Bonuses:**
- **+100**: Keyword matches in folder name (not just parent path)
- **+200**: Exact folder name match
- **+50**: Folder name starts with keyword
- **+10**: Keyword only matches in parent path
- **+0 to +50**: Earlier matches in path get higher scores
**Path-Based Adjustments:**
- **-5 per level**: Penalty for nested depth (prefers shallower folders)
- **+0 to +100**: Bonus for shorter paths (more specific matches)
#### 3. Parent-Child Deduplication
After initial scoring, the algorithm applies deduplication:
```typescript
// Count total occurrences of all keywords in both parent and child
let parentOccurrences = 0;
let childOccurrences = 0;
for (const keyword of keywords) {
// Count occurrences of this keyword in parent
let pos = 0;
while ((pos = parentLower.indexOf(keyword, pos)) !== -1) {
parentOccurrences++;
pos += keyword.length;
}
// Count occurrences of this keyword in child
pos = 0;
while ((pos = childLower.indexOf(keyword, pos)) !== -1) {
childOccurrences++;
pos += keyword.length;
}
}
// If child doesn't have more keyword occurrences than parent, exclude it
if (childOccurrences <= parentOccurrences) {
shouldExclude = true;
}
```
This ensures that when searching for "abc":
- "abc" is shown (1 occurrence)
- "abc/xxx" is completely hidden (still 1 occurrence of "abc")
- "abc/xxx/abc" would be shown (2 occurrences of "abc")
#### 4. Result Limiting
Results are sorted by final score and limited to the `maxResults` setting.
## Examples
### Example 1: Out-of-Order Keywords (Issue #8)
**Query:** "bar foo"
**Matches:**
- ✅ `foo/bar/baz` - Both keywords present
- ✅ `projects/foo/notes/bar` - Both keywords present
- ❌ `foo/test` - Missing "bar" keyword
- ❌ `bar/test` - Missing "foo" keyword
### Example 2: Parent-Child Prioritization (Issue #4)
**Query:** "abc"
**Before (old behavior):**
1. `abc` (score: 350)
2. `abc/xxx` (score: 340)
3. `abc/yyy` (score: 340)
4. `abc/zzz` (score: 340)
5. `projects/abc` (score: 150)
**After (new behavior):**
1. `abc` (score: 350)
2. `projects/abc` (score: 150)
3. (Other non-related folders...)
Note: `abc/xxx`, `abc/yyy`, `abc/zzz` are completely excluded from results
### Example 3: Additional Keyword in Child
**Query:** "abc test"
**Results:**
1. `abc/test` (score: 450) - Matches both keywords, shown
2. `abc` (score: 350) - Only matches "abc", shown
3. `projects/abc/test` (score: 340) - Matches both, shown
4. Other folders matching both keywords...
Note: `abc/test` is shown because it has 2 total keyword occurrences ("abc" + "test"), more than its parent `abc` which only has 1. However, `abc/notes` would be excluded because it still only has 1 occurrence of "abc", the same count as its parent.
### Example 4: Repeated Keywords
**Query:** "ai prompt"
**Folder Structure:**
- `AI/Prompt engineering` - Contains "ai" (1x) and "prompt" (1x) = 2 total occurrences
- `AI/Prompt engineering/Prompt engineering` - Contains "ai" (1x) and "prompt" (2x) = 3 total occurrences
- `AI/Prompt engineering/asset` - Contains "ai" (1x) and "prompt" (1x) = 2 total occurrences
**Results:**
1. `AI/Prompt engineering` - Shown (2 occurrences)
2. `AI/Prompt engineering/Prompt engineering` - Shown (3 occurrences, more than parent)
3. `AI/Prompt engineering/asset` - Hidden (2 occurrences, same as parent)
## Technical Notes
### Type Safety
- Added `FolderWithScore` interface for internal scoring
- Maintains compatibility with Obsidian's `FuzzyMatch<TFolder>` type
- Proper handling of separator items
### Debug Support
- All search operations respect the `debugMode` setting
- Logs query, keywords, and result counts when debug mode is enabled
- Useful for troubleshooting search behavior
### Performance Considerations
- Iterates through all folders once for scoring
- Uses Set for efficient parent path lookups
- Limits results to prevent UI overload
- Complexity: O(n * k) where n = folders, k = keywords
## Future Improvements
Potential enhancements for consideration:
1. **Fuzzy character matching**: Allow typos and approximate matches
2. **History-weighted scoring**: Boost scores for recently/frequently accessed folders
3. **Custom weights**: Let users adjust scoring parameters
4. **Regex support**: Enable advanced pattern matching
5. **Path segment prioritization**: Give higher weight to matches in specific segments

View file

@ -1,7 +1,7 @@
{
"id": "folder-navigator",
"name": "Folder Navigator",
"version": "0.2.0",
"version": "0.5.0",
"minAppVersion": "1.5.3",
"description": "Quickly navigate to folders in your vault using fuzzy search.",
"author": "wenlzhang",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "plugin-template",
"version": "0.2.0",
"version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "plugin-template",
"version": "0.2.0",
"version": "0.5.0",
"dependencies": {
"tslib": "^2.8.1"
},

View file

@ -1,6 +1,6 @@
{
"name": "folder-navigator",
"version": "0.2.0",
"version": "0.5.0",
"description": "Quickly navigate to folders in your vault using fuzzy search.",
"main": "main.js",
"scripts": {

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,35 @@
import { App } from "obsidian";
export enum FolderDisplayMode {
DEFAULT = "default",
RECENCY = "recency",
FREQUENCY = "frequency",
}
export interface PluginSettings {
maxResults: number;
expandTargetFolder: boolean;
debugMode: boolean;
folderDisplayMode: FolderDisplayMode;
recentFoldersToShow: number;
frequentFoldersToShow: number;
folderHistory: Record<
string,
{
lastAccessed: number; // timestamp
accessCount: number; // visit count
}
>;
newFolderLocation: string; // Path where new folders should be created (empty string = root)
}
export const DEFAULT_SETTINGS: PluginSettings = {
maxResults: 10,
expandTargetFolder: true, // Enable by default for better navigation
debugMode: false, // Disabled by default
folderDisplayMode: FolderDisplayMode.DEFAULT,
recentFoldersToShow: 5,
frequentFoldersToShow: 5,
folderHistory: {},
newFolderLocation: "", // Empty string means root folder
};

View file

@ -1,18 +1,171 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import {
App,
PluginSettingTab,
Setting,
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<TFolder> {
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;
// Keep track of settings elements for dynamic updates
recentFoldersSettingEl: HTMLElement;
frequentFoldersSettingEl: HTMLElement;
resetHistorySettingEl: HTMLElement;
constructor(app: App, plugin: FolderNavigatorPlugin) {
super(app, plugin);
this.plugin = plugin;
}
/**
* Updates visibility of settings based on the current display mode
*/
updateSettingsVisibility(displayMode: FolderDisplayMode): void {
// Create settings elements if they don't exist yet
if (
!this.recentFoldersSettingEl ||
!this.frequentFoldersSettingEl ||
!this.resetHistorySettingEl
) {
return;
}
// Handle recent folders setting
if (displayMode === FolderDisplayMode.RECENCY) {
this.recentFoldersSettingEl.style.display = "block";
this.frequentFoldersSettingEl.style.display = "none";
} else if (displayMode === FolderDisplayMode.FREQUENCY) {
this.recentFoldersSettingEl.style.display = "none";
this.frequentFoldersSettingEl.style.display = "block";
} else {
// Default mode - hide both
this.recentFoldersSettingEl.style.display = "none";
this.frequentFoldersSettingEl.style.display = "none";
}
// Only show reset history button for non-default modes
if (displayMode === FolderDisplayMode.DEFAULT) {
this.resetHistorySettingEl.style.display = "none";
} else {
this.resetHistorySettingEl.style.display = "block";
}
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// Folder display
new Setting(containerEl).setName("Folder display").setHeading();
// Display priority
new Setting(containerEl)
.setName("Display priority")
.setDesc(
"Control which folders appear at the top of the suggestion list before searching",
)
.addDropdown((dropdown: DropdownComponent) => {
dropdown
.addOption(
FolderDisplayMode.DEFAULT,
"Default order (alphabetical)",
)
.addOption(
FolderDisplayMode.RECENCY,
"Recently visited first",
)
.addOption(
FolderDisplayMode.FREQUENCY,
"Frequently visited first",
)
.setValue(this.plugin.settings.folderDisplayMode)
.onChange(async (value: string) => {
this.plugin.settings.folderDisplayMode =
value as FolderDisplayMode;
await this.plugin.saveSettings();
// Update the visibility of settings when selection changes
this.updateSettingsVisibility(
value as FolderDisplayMode,
);
});
});
// Recent folders
this.recentFoldersSettingEl = containerEl.createDiv();
new Setting(this.recentFoldersSettingEl)
.setName("Recent folders to show")
.setDesc(
"Number of recently visited folders to display at the top when using 'Recently visited first' mode",
)
.addSlider((slider) =>
slider
.setLimits(1, 20, 1)
.setValue(this.plugin.settings.recentFoldersToShow)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.recentFoldersToShow = value;
await this.plugin.saveSettings();
}),
);
// Frequent folders
this.frequentFoldersSettingEl = containerEl.createDiv();
new Setting(this.frequentFoldersSettingEl)
.setName("Frequent folders to show")
.setDesc(
"Number of frequently visited folders to display at the top when using 'Frequently visited first' mode",
)
.addSlider((slider) =>
slider
.setLimits(1, 20, 1)
.setValue(this.plugin.settings.frequentFoldersToShow)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.frequentFoldersToShow = value;
await this.plugin.saveSettings();
}),
);
// Maximum results
new Setting(containerEl)
.setName("Maximum results")
.setDesc("Maximum number of folders to show in search results")
@ -27,6 +180,99 @@ export class SettingsTab extends PluginSettingTab {
}),
);
// Reset history
this.resetHistorySettingEl = containerEl.createDiv();
new Setting(this.resetHistorySettingEl)
.setName("Reset folder history")
.setDesc(
"Clear the tracked history of visited folders. Warning: This will remove all recency and frequency data.",
)
.addButton((button) =>
button
.setButtonText("Reset")
.setWarning()
.onClick(async () => {
// Create a confirmation modal
const confirmModal = new Modal(this.app);
confirmModal.titleEl.setText("Reset folder history");
confirmModal.contentEl.createEl("p", {
text: "Are you sure? This will permanently delete all folder navigation history including recently visited and frequently used folder data.",
cls: "mod-warning",
});
const buttonContainer =
confirmModal.contentEl.createDiv({
cls: "modal-button-container",
});
// Cancel button
buttonContainer
.createEl("button", {
text: "Cancel",
})
.addEventListener("click", () => {
confirmModal.close();
});
// Confirm button
const confirmButton = buttonContainer.createEl(
"button",
{
text: "Reset folder history",
cls: "mod-warning",
},
);
confirmButton.addEventListener("click", async () => {
this.plugin.settings.folderHistory = {};
await this.plugin.saveSettings();
confirmModal.close();
new Notice("Folder history has been reset");
});
confirmModal.open();
}),
);
// New folder location
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 navigation
new Setting(containerEl).setName("Folder navigation").setHeading();
new Setting(containerEl)
.setName("Expand target folder on navigation")
.setDesc(
@ -40,5 +286,25 @@ export class SettingsTab extends PluginSettingTab {
await this.plugin.saveSettings();
}),
);
// Initialize settings visibility based on current display mode
this.updateSettingsVisibility(this.plugin.settings.folderDisplayMode);
// Add advanced section
new Setting(containerEl).setName("Advanced").setHeading();
new Setting(containerEl)
.setName("Debug mode")
.setDesc(
"Enable detailed logging to console for troubleshooting issues. This may affect performance and should be disabled during normal use.",
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.debugMode)
.onChange(async (value) => {
this.plugin.settings.debugMode = value;
await this.plugin.saveSettings();
}),
);
}
}

View file

@ -3,3 +3,34 @@
border-radius: 4px;
transition: background-color 0.3s ease;
}
.folder-separator {
color: var(--text-muted);
font-size: 0.85em;
text-align: center;
padding: 5px 0;
font-style: italic;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 5px;
pointer-events: none;
}
.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;
}

View file

@ -1,5 +1,11 @@
{
"0.0.1": "0.0.1",
"0.1.0": "1.0.0",
"0.2.0": "1.5.3"
"0.2.0": "1.5.3",
"0.2.1": "1.5.3",
"0.3.0": "1.5.3",
"0.3.1": "1.5.3",
"0.4.0": "1.5.3",
"0.4.1": "1.5.3",
"0.5.0": "1.5.3"
}