mirror of
https://github.com/wenlzhang/obsidian-folder-navigator.git
synced 2026-07-22 05:41:23 +00:00
Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4175b9dc30 | ||
|
|
28db3e9d50 | ||
|
|
d6f18b52c2 | ||
|
|
95e5d2937f | ||
|
|
5780b3e8bc | ||
|
|
b1af049004 | ||
|
|
658adcbfe4 | ||
|
|
89df8eb024 | ||
|
|
d6de79d372 | ||
|
|
97974cc801 | ||
|
|
3b440f3222 | ||
|
|
bf52a4b2a5 | ||
|
|
78f03608ea | ||
|
|
1dd71601ce | ||
|
|
71294a3b94 | ||
|
|
5929cd9cf2 |
11 changed files with 1003 additions and 181 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -9,6 +9,42 @@ 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
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -9,15 +9,19 @@ An [Obsidian](https://obsidian.md) plugin that allows you to quickly navigate to
|
|||
## Features
|
||||
|
||||
- Quick folder navigation using fuzzy search
|
||||
- Configurable maximum number of search results
|
||||
- Expand target folder on navigation
|
||||
- Works with nested folders
|
||||
- Smart folder sorting options:
|
||||
- Default sorting (alphabetical)
|
||||
- Recently visited folders displayed at the top
|
||||
- Frequently visited folders displayed at the top
|
||||
- Customizable number of recent/frequent folders to show
|
||||
- Visual separators to distinguish recent/frequent folders
|
||||
- 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
|
||||
|
||||
|
||||
## Videos and Articles
|
||||
|
||||
### Articles
|
||||
|
||||
- [The Strategy of Note Taking: Folders, Tags, Links, and Redundancy - PTKM](https://ptkm.net/blog-note-taking-strategy-folders-tags-links-redundancy)
|
||||
|
||||
## Why Folder Navigator?
|
||||
|
||||
|
|
|
|||
169
docs/implementation/SEARCH_IMPROVEMENTS.md
Normal file
169
docs/implementation/SEARCH_IMPROVEMENTS.md
Normal 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "folder-navigator",
|
||||
"name": "Folder Navigator",
|
||||
"version": "0.3.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
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "plugin-template",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "plugin-template",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "folder-navigator",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Quickly navigate to folders in your vault using fuzzy search.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { App, FuzzySuggestModal, TFolder, WorkspaceLeaf, View, FuzzyMatch } from "obsidian";
|
||||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TFolder,
|
||||
WorkspaceLeaf,
|
||||
View,
|
||||
FuzzyMatch,
|
||||
Notice,
|
||||
} from "obsidian";
|
||||
import type FolderNavigatorPlugin from "./main";
|
||||
import { FolderSortMode } from "./settings";
|
||||
import { FolderDisplayMode } from "./settings";
|
||||
|
||||
// Helper function for conditional logging based on plugin settings
|
||||
function log(
|
||||
|
|
@ -40,6 +48,17 @@ interface ExtendedApp extends App {
|
|||
internalPlugins: InternalPlugins;
|
||||
}
|
||||
|
||||
interface FolderWithScore {
|
||||
folder: TFolder;
|
||||
score: number;
|
||||
match: FuzzyMatch<TFolder>;
|
||||
}
|
||||
|
||||
interface CreateFolderOption extends TFolder {
|
||||
_isCreateOption: boolean;
|
||||
_folderName: string;
|
||||
}
|
||||
|
||||
export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
||||
folders: TFolder[];
|
||||
plugin: FolderNavigatorPlugin;
|
||||
|
|
@ -61,28 +80,30 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sort folders based on the selected sort mode in settings
|
||||
* Sort folders based on the selected display mode in settings
|
||||
*/
|
||||
private getSortedFolders(): TFolder[] {
|
||||
const allFolders = this.getAllFolders();
|
||||
const sortMode = this.plugin.settings.folderSortMode;
|
||||
|
||||
log(`Sorting folders using mode: ${sortMode}`, this.plugin);
|
||||
|
||||
const displayMode = this.plugin.settings.folderDisplayMode;
|
||||
|
||||
log(`Sorting folders using display mode: ${displayMode}`, this.plugin);
|
||||
|
||||
// Default sorting (alphabetical by path)
|
||||
if (sortMode === FolderSortMode.DEFAULT) {
|
||||
if (displayMode === FolderDisplayMode.DEFAULT) {
|
||||
return allFolders;
|
||||
}
|
||||
|
||||
|
||||
// Get folder history from settings
|
||||
const folderHistory = this.plugin.settings.folderHistory;
|
||||
|
||||
if (sortMode === FolderSortMode.RECENCY) {
|
||||
|
||||
if (displayMode === FolderDisplayMode.RECENCY) {
|
||||
// Create a copy of all folders to sort
|
||||
const recentLimit = this.plugin.settings.recentFoldersToShow;
|
||||
|
||||
|
||||
// Find folders with history and sort by last accessed timestamp (descending)
|
||||
const foldersWithHistory = allFolders.filter(f => folderHistory[f.path]);
|
||||
const foldersWithHistory = allFolders.filter(
|
||||
(f) => folderHistory[f.path],
|
||||
);
|
||||
const recentFolders = foldersWithHistory
|
||||
.sort((a, b) => {
|
||||
const timeA = folderHistory[a.path]?.lastAccessed || 0;
|
||||
|
|
@ -90,39 +111,52 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
return timeB - timeA; // Descending order (newest first)
|
||||
})
|
||||
.slice(0, recentLimit);
|
||||
|
||||
|
||||
// Get the remaining folders (those without history or beyond the limit)
|
||||
const recentFolderPaths = new Set(recentFolders.map(f => f.path));
|
||||
const remainingFolders = allFolders.filter(f => !recentFolderPaths.has(f.path));
|
||||
|
||||
log(`Found ${recentFolders.length} recent folders to display`, this.plugin);
|
||||
|
||||
const recentFolderPaths = new Set(recentFolders.map((f) => f.path));
|
||||
const remainingFolders = allFolders.filter(
|
||||
(f) => !recentFolderPaths.has(f.path),
|
||||
);
|
||||
|
||||
log(
|
||||
`Found ${recentFolders.length} recent folders to display`,
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// Only add separator if we have recent folders to show
|
||||
if (recentFolders.length > 0) {
|
||||
// Create a separator "folder" (not a real folder)
|
||||
const separator = {} as TFolder;
|
||||
(separator as any)._isSeparator = true;
|
||||
(separator as any)._separatorText = "— Recently visited folders —";
|
||||
|
||||
(separator as any)._separatorText =
|
||||
"— Recently visited folders —";
|
||||
|
||||
// Create another separator for the remaining folders
|
||||
const remainingSeparator = {} as TFolder;
|
||||
(remainingSeparator as any)._isSeparator = true;
|
||||
(remainingSeparator as any)._separatorText = "— All folders —";
|
||||
|
||||
|
||||
// Combine recent folders with remaining folders with separators
|
||||
return [separator, ...recentFolders, remainingSeparator, ...remainingFolders];
|
||||
return [
|
||||
separator,
|
||||
...recentFolders,
|
||||
remainingSeparator,
|
||||
...remainingFolders,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// If no recent folders, just return all folders
|
||||
return allFolders;
|
||||
}
|
||||
|
||||
if (sortMode === FolderSortMode.FREQUENCY) {
|
||||
|
||||
if (displayMode === FolderDisplayMode.FREQUENCY) {
|
||||
// Create a copy of all folders to sort
|
||||
const frequentLimit = this.plugin.settings.frequentFoldersToShow;
|
||||
|
||||
|
||||
// Find folders with history and sort by access count (descending)
|
||||
const foldersWithHistory = allFolders.filter(f => folderHistory[f.path]);
|
||||
const foldersWithHistory = allFolders.filter(
|
||||
(f) => folderHistory[f.path],
|
||||
);
|
||||
const frequentFolders = foldersWithHistory
|
||||
.sort((a, b) => {
|
||||
const countA = folderHistory[a.path]?.accessCount || 0;
|
||||
|
|
@ -130,33 +164,46 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
return countB - countA; // Descending order (most frequent first)
|
||||
})
|
||||
.slice(0, frequentLimit);
|
||||
|
||||
|
||||
// Get the remaining folders (those without history or beyond the limit)
|
||||
const frequentFolderPaths = new Set(frequentFolders.map(f => f.path));
|
||||
const remainingFolders = allFolders.filter(f => !frequentFolderPaths.has(f.path));
|
||||
|
||||
log(`Found ${frequentFolders.length} frequent folders to display`, this.plugin);
|
||||
|
||||
const frequentFolderPaths = new Set(
|
||||
frequentFolders.map((f) => f.path),
|
||||
);
|
||||
const remainingFolders = allFolders.filter(
|
||||
(f) => !frequentFolderPaths.has(f.path),
|
||||
);
|
||||
|
||||
log(
|
||||
`Found ${frequentFolders.length} frequent folders to display`,
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// Only add separator if we have frequent folders to show
|
||||
if (frequentFolders.length > 0) {
|
||||
// Create a separator "folder" (not a real folder)
|
||||
const separator = {} as TFolder;
|
||||
(separator as any)._isSeparator = true;
|
||||
(separator as any)._separatorText = "— Frequently visited folders —";
|
||||
|
||||
(separator as any)._separatorText =
|
||||
"— Frequently visited folders —";
|
||||
|
||||
// Create another separator for the remaining folders
|
||||
const remainingSeparator = {} as TFolder;
|
||||
(remainingSeparator as any)._isSeparator = true;
|
||||
(remainingSeparator as any)._separatorText = "— All folders —";
|
||||
|
||||
|
||||
// Combine frequent folders with remaining folders with separators
|
||||
return [separator, ...frequentFolders, remainingSeparator, ...remainingFolders];
|
||||
return [
|
||||
separator,
|
||||
...frequentFolders,
|
||||
remainingSeparator,
|
||||
...remainingFolders,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// If no frequent folders, just return all folders
|
||||
return allFolders;
|
||||
}
|
||||
|
||||
|
||||
// Fallback to default sorting
|
||||
return allFolders;
|
||||
}
|
||||
|
|
@ -167,22 +214,286 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
private updateFolderHistory(folder: TFolder): void {
|
||||
// Get current history or create new entry
|
||||
const folderPath = folder.path;
|
||||
const history = this.plugin.settings.folderHistory[folderPath] || {
|
||||
const history = this.plugin.settings.folderHistory[folderPath] || {
|
||||
lastAccessed: 0,
|
||||
accessCount: 0
|
||||
accessCount: 0,
|
||||
};
|
||||
|
||||
|
||||
// Update history data
|
||||
history.lastAccessed = Date.now();
|
||||
history.accessCount += 1;
|
||||
|
||||
|
||||
// Save back to settings
|
||||
this.plugin.settings.folderHistory[folderPath] = history;
|
||||
|
||||
|
||||
// Save settings
|
||||
this.plugin.saveSettings();
|
||||
|
||||
log(`Updated folder history for "${folderPath}": ${JSON.stringify(history)}`, this.plugin);
|
||||
|
||||
log(
|
||||
`Updated folder history for "${folderPath}": ${JSON.stringify(history)}`,
|
||||
this.plugin,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom search implementation with improved fuzzy matching
|
||||
* Supports out-of-order keyword matching and better scoring
|
||||
*/
|
||||
getSuggestions(query: string): FuzzyMatch<TFolder>[] {
|
||||
// If no query, return all items (or recent/frequent items based on settings)
|
||||
if (!query || query.trim().length === 0) {
|
||||
return this.sortedFolders.map((folder) => ({
|
||||
item: folder,
|
||||
match: { score: 0, matches: [] },
|
||||
}));
|
||||
}
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
|
||||
// Split query into keywords (space-separated)
|
||||
const keywords = normalizedQuery
|
||||
.split(/\s+/)
|
||||
.filter((k) => k.length > 0);
|
||||
|
||||
log(
|
||||
`Search query: "${query}", keywords: [${keywords.join(", ")}]`,
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
// Score each folder
|
||||
const foldersWithScore: FolderWithScore[] = [];
|
||||
|
||||
for (const folder of this.sortedFolders) {
|
||||
// Skip separators
|
||||
if ((folder as any)._isSeparator) {
|
||||
foldersWithScore.push({
|
||||
folder,
|
||||
score: -1,
|
||||
match: { item: folder, match: { score: 0, matches: [] } },
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderPath = folder.path.toLowerCase();
|
||||
const folderName = folder.name.toLowerCase();
|
||||
const originalPath = folder.path; // Keep original case for match positions
|
||||
|
||||
// Check if all keywords match (in any order)
|
||||
let allKeywordsMatch = true;
|
||||
let totalScore = 0;
|
||||
const matchPositions: Array<[number, number]> = [];
|
||||
|
||||
for (const keyword of keywords) {
|
||||
const pathIndex = folderPath.indexOf(keyword);
|
||||
const nameIndex = folderName.indexOf(keyword);
|
||||
|
||||
if (pathIndex === -1) {
|
||||
// Keyword doesn't match this folder at all
|
||||
allKeywordsMatch = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Store match positions for highlighting (using original path positions)
|
||||
// Find ALL occurrences of this keyword in the original case-sensitive path
|
||||
const originalPathLower = originalPath.toLowerCase();
|
||||
let pos = 0;
|
||||
while ((pos = originalPathLower.indexOf(keyword, pos)) !== -1) {
|
||||
// Record character positions for this occurrence
|
||||
for (let i = 0; i < keyword.length; i++) {
|
||||
matchPositions.push([pos + i, pos + i]);
|
||||
}
|
||||
pos += keyword.length; // Move past this occurrence to find the next one
|
||||
}
|
||||
|
||||
// Calculate score for this keyword
|
||||
let keywordScore = 0;
|
||||
|
||||
// Bonus for matching in folder name (not just path)
|
||||
if (nameIndex !== -1) {
|
||||
keywordScore += 100;
|
||||
|
||||
// Extra bonus for exact name match
|
||||
if (folderName === keyword) {
|
||||
keywordScore += 200;
|
||||
}
|
||||
// Bonus for name starting with keyword
|
||||
else if (nameIndex === 0) {
|
||||
keywordScore += 50;
|
||||
}
|
||||
} else {
|
||||
// Match is in parent path, lower score
|
||||
keywordScore += 10;
|
||||
}
|
||||
|
||||
// Bonus for match position (earlier is better)
|
||||
keywordScore += Math.max(0, 50 - pathIndex);
|
||||
|
||||
totalScore += keywordScore;
|
||||
}
|
||||
|
||||
if (allKeywordsMatch) {
|
||||
// Additional scoring adjustments
|
||||
|
||||
// Penalize deeper nested folders
|
||||
const depth = folder.path.split("/").length;
|
||||
totalScore -= depth * 5;
|
||||
|
||||
// Bonus for shorter paths (more specific)
|
||||
totalScore += Math.max(0, 100 - folder.path.length);
|
||||
|
||||
foldersWithScore.push({
|
||||
folder,
|
||||
score: totalScore,
|
||||
match: {
|
||||
item: folder,
|
||||
match: {
|
||||
score: totalScore,
|
||||
matches: matchPositions,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score (descending)
|
||||
foldersWithScore.sort((a, b) => {
|
||||
// Keep separators at their original positions
|
||||
if (
|
||||
(a.folder as any)._isSeparator ||
|
||||
(b.folder as any)._isSeparator
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return b.score - a.score;
|
||||
});
|
||||
|
||||
// Apply parent folder deduplication for Issue #4
|
||||
// When a parent folder matches, completely exclude its children unless they have additional keyword matches
|
||||
const results: FuzzyMatch<TFolder>[] = [];
|
||||
const parentPaths = new Set<string>();
|
||||
|
||||
for (const item of foldersWithScore) {
|
||||
// Always include separators
|
||||
if ((item.folder as any)._isSeparator) {
|
||||
results.push(item.match);
|
||||
continue;
|
||||
}
|
||||
|
||||
const folderPath = item.folder.path;
|
||||
|
||||
// Check if this folder's parent is already in results
|
||||
let shouldExclude = false;
|
||||
let isChildOfMatch = false;
|
||||
|
||||
for (const parentPath of parentPaths) {
|
||||
// Check if current folder is a child of an already matched parent
|
||||
if (folderPath.startsWith(parentPath + "/")) {
|
||||
isChildOfMatch = true;
|
||||
|
||||
// Check if this child has additional keyword occurrences beyond the parent
|
||||
// Count total occurrences of all keywords in both parent and child
|
||||
const parentLower = parentPath.toLowerCase();
|
||||
const childLower = folderPath.toLowerCase();
|
||||
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only include if not excluded
|
||||
if (!shouldExclude) {
|
||||
results.push(item.match);
|
||||
|
||||
// Add this folder to parent paths for future checks
|
||||
if (!isChildOfMatch) {
|
||||
parentPaths.add(folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit results
|
||||
const limitedResults = results.slice(0, this.limit);
|
||||
|
||||
log(
|
||||
`Found ${limitedResults.length} matching folders (filtered from ${foldersWithScore.length} total matches)`,
|
||||
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;
|
||||
}
|
||||
|
||||
getItems(): TFolder[] {
|
||||
|
|
@ -194,6 +505,10 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -259,25 +574,54 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
log(
|
||||
`Folder selected: "${folder.path}" (name: "${folder.name}")`,
|
||||
this.plugin,
|
||||
);
|
||||
private async handleCreateFolder(folderName: string): Promise<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
|
|
@ -352,6 +696,35 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
@ -484,13 +857,70 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
|||
if ((item.item as any)._isSeparator) {
|
||||
el.empty();
|
||||
const separatorEl = el.createDiv({
|
||||
cls: "folder-separator"
|
||||
cls: "folder-separator",
|
||||
});
|
||||
separatorEl.setText((item.item as any)._separatorText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular folder rendering - use the default rendering
|
||||
super.renderSuggestion(item, el);
|
||||
|
||||
// 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;
|
||||
const folderPath = folder.path;
|
||||
|
||||
// Get match positions from the fuzzy match result
|
||||
const matches = item.match?.matches || [];
|
||||
|
||||
if (matches.length === 0) {
|
||||
// No matches, just display the text normally
|
||||
el.setText(folderPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort matches by position
|
||||
const sortedMatches = matches.slice().sort((a, b) => a[0] - b[0]);
|
||||
|
||||
// Build the highlighted text
|
||||
let lastIndex = 0;
|
||||
const textEl = el.createDiv({ cls: "suggestion-content" });
|
||||
|
||||
for (const match of sortedMatches) {
|
||||
const [start, end] = match;
|
||||
|
||||
// Add text before the match
|
||||
if (start > lastIndex) {
|
||||
textEl.appendText(folderPath.substring(lastIndex, start));
|
||||
}
|
||||
|
||||
// Add the matched text with highlighting
|
||||
const matchEl = textEl.createSpan({ cls: "suggestion-highlight" });
|
||||
matchEl.setText(folderPath.substring(start, end + 1));
|
||||
|
||||
lastIndex = end + 1;
|
||||
}
|
||||
|
||||
// Add remaining text after the last match
|
||||
if (lastIndex < folderPath.length) {
|
||||
textEl.appendText(folderPath.substring(lastIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,35 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
export enum FolderSortMode {
|
||||
export enum FolderDisplayMode {
|
||||
DEFAULT = "default",
|
||||
RECENCY = "recency",
|
||||
FREQUENCY = "frequency"
|
||||
FREQUENCY = "frequency",
|
||||
}
|
||||
|
||||
export interface PluginSettings {
|
||||
maxResults: number;
|
||||
expandTargetFolder: boolean;
|
||||
debugMode: boolean;
|
||||
folderSortMode: FolderSortMode;
|
||||
folderDisplayMode: FolderDisplayMode;
|
||||
recentFoldersToShow: number;
|
||||
frequentFoldersToShow: number;
|
||||
folderHistory: Record<string, {
|
||||
lastAccessed: number; // timestamp
|
||||
accessCount: number; // visit count
|
||||
}>;
|
||||
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
|
||||
folderSortMode: FolderSortMode.DEFAULT,
|
||||
folderDisplayMode: FolderDisplayMode.DEFAULT,
|
||||
recentFoldersToShow: 5,
|
||||
frequentFoldersToShow: 5,
|
||||
folderHistory: {},
|
||||
newFolderLocation: "", // Empty string means root folder
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,171 @@
|
|||
import { App, PluginSettingTab, Setting, DropdownComponent, Modal, Notice } from "obsidian";
|
||||
import {
|
||||
App,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
DropdownComponent,
|
||||
Modal,
|
||||
Notice,
|
||||
FuzzySuggestModal,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import FolderNavigatorPlugin from "./main";
|
||||
import { FolderSortMode } from "./settings";
|
||||
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();
|
||||
|
||||
// General settings section
|
||||
// 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")
|
||||
|
|
@ -29,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(
|
||||
|
|
@ -43,98 +287,8 @@ export class SettingsTab extends PluginSettingTab {
|
|||
}),
|
||||
);
|
||||
|
||||
// Folder sorting section
|
||||
new Setting(containerEl).setName("Folder sorting").setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Sort mode")
|
||||
.setDesc("How folders are sorted in the suggestion list")
|
||||
.addDropdown((dropdown: DropdownComponent) => {
|
||||
dropdown
|
||||
.addOption(FolderSortMode.DEFAULT, "Default (alphabetical)")
|
||||
.addOption(FolderSortMode.RECENCY, "Recently visited")
|
||||
.addOption(FolderSortMode.FREQUENCY, "Frequently visited")
|
||||
.setValue(this.plugin.settings.folderSortMode)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.folderSortMode = value as FolderSortMode;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Only show these settings if the sort mode is not DEFAULT
|
||||
if (this.plugin.settings.folderSortMode !== FolderSortMode.DEFAULT) {
|
||||
new Setting(containerEl)
|
||||
.setName("Recent folders to show")
|
||||
.setDesc("Number of recently visited folders to display at the top when using the recency sort 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();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Frequent folders to show")
|
||||
.setDesc("Number of frequently visited folders to display at the top when using the frequency sort 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();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Add reset history button
|
||||
new Setting(containerEl)
|
||||
.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();
|
||||
}),
|
||||
);
|
||||
// Initialize settings visibility based on current display mode
|
||||
this.updateSettingsVisibility(this.plugin.settings.folderDisplayMode);
|
||||
|
||||
// Add advanced section
|
||||
new Setting(containerEl).setName("Advanced").setHeading();
|
||||
|
|
|
|||
20
styles.css
20
styles.css
|
|
@ -14,3 +14,23 @@
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@
|
|||
"0.1.0": "1.0.0",
|
||||
"0.2.0": "1.5.3",
|
||||
"0.2.1": "1.5.3",
|
||||
"0.3.0": "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"
|
||||
}
|
||||
Loading…
Reference in a new issue