docs: correct claims contradicted by the source, extend QA checklist
DOCUMENTATION.md said recent files were in-memory only and lost on vault close, while the code persists them under waypointData.recentFiles; the WaypointData/RecentFilesSettings/WaypointSettings snippets were also missing real fields. Fix those, describe the new behaviour (single-writer saves, settings-driven period detection, O(1) basename lookups, folder-aware rename remapping, unified openPeriodNote, typechecked build), and trim Known Limitations to the two still true (full-DOM redraw, CDN icon dependency). QA.md: restate the 'No icon' case and add coverage for week-number middle-click and for moving a folder that contains bookmarked files.
This commit is contained in:
+64
-43
@@ -10,18 +10,19 @@
|
||||
src/
|
||||
├── main.ts Plugin entry — lifecycle, commands, events, data persistence
|
||||
├── settings.ts Interface definitions + DEFAULT_SETTINGS constant
|
||||
├── settings-tab.ts PluginSettingTab UI (calendar, periodic paths, recent files)
|
||||
├── settings-tab.ts PluginSettingTab UI — tabs: calendar, periodic, recent, display, about
|
||||
├── models/
|
||||
│ └── bookmark.ts BookmarkItem + WaypointData interfaces
|
||||
├── utils/
|
||||
│ └── date-utils.ts Moment.js helpers: period formatting, month grid, navigation
|
||||
│ ├── date-utils.ts Moment.js helpers: period formatting, month grid, navigation
|
||||
│ └── path-utils.ts Pure path helper: rename remapping (no `obsidian` import, unit-testable)
|
||||
└── views/
|
||||
└── waypoint-view.ts ItemView subclass — full sidebar rendering + interaction
|
||||
```
|
||||
|
||||
**Build:** esbuild bundles `src/main.ts` → `main.js`. TypeScript strict, ESNext target.
|
||||
**Build:** `npm run build` runs `tsc --noEmit` first, then esbuild bundles `src/main.ts` → `main.js`. The typecheck is a real gate — esbuild strips types without checking them, so without it type errors would ship. `npm run typecheck` runs the check alone; `npm run dev` (watch) stays ungated for speed. TypeScript strict-null + `noImplicitAny`, ESNext modules, ES6 target, `lib` floor at ES2017.
|
||||
|
||||
**Data persistence:** Single `data.json` via Obsidian `Plugin.loadData()/saveData()`. Top-level keys: `settings` (merged with `DEFAULT_SETTINGS`), `waypointData` (bookmark tree). Recent files are in-memory only (lost on vault close).
|
||||
**Data persistence:** Single `data.json` via Obsidian `Plugin.loadData()/saveData()`. Top-level keys: `settings` (merged with `DEFAULT_SETTINGS`), `waypointData` (bookmark tree **and** recent files). Recent files are persisted under `waypointData.recentFiles` and reloaded on startup; writes are debounced 300ms so rapid file opens collapse into one save.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,7 +34,7 @@ src/
|
||||
2. **Register view** — `WAYPOINT_VIEW_TYPE = "waypoint-view"` maps to `WaypointView` factory `(leaf) => new WaypointView(leaf, this)`.
|
||||
3. **Settings tab** — `WaypointSettingTab` receives the live `settings` object + `() => this.redrawAll()` callback for live preview.
|
||||
4. **Register commands** (see Commands section below).
|
||||
5. **Register events** — `file-open`, `vault:create`, `vault:delete`, `vault:rename`.
|
||||
5. **Register events** — `file-open`, `vault:create`, `vault:delete`, `vault:rename`, `vault:modify`.
|
||||
6. **Auto-open** — On `onLayoutReady`, if no existing leaves of the view type, opens one in the left sidebar.
|
||||
7. **Midnight refresh** — 10-minute `setInterval` checks if `new Date().toDateString()` changed; if so, calls `redrawAll()` (calendar day indicators refresh).
|
||||
|
||||
@@ -55,7 +56,8 @@ interface WaypointSettings {
|
||||
monthly: PeriodNoteSettings;
|
||||
quarterly: PeriodNoteSettings;
|
||||
yearly: PeriodNoteSettings;
|
||||
recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[]
|
||||
recentFiles: RecentFilesSettings; // maxItems, updateOn, omittedPaths[], omittedTags[], filterTags[]
|
||||
display: DisplaySettings; // px sizing for bookmark rows, fonts, icons, calendar cells
|
||||
}
|
||||
|
||||
interface PeriodNoteSettings {
|
||||
@@ -74,7 +76,17 @@ interface RecentFilesSettings {
|
||||
maxItems: number; // default 50
|
||||
updateOn: 'file-open' | 'file-edit'; // trigger mode
|
||||
omittedPaths: string[]; // regex patterns (one per line)
|
||||
omittedTags: string[]; // regex patterns for frontmatter tags
|
||||
omittedTags: string[]; // regex patterns for frontmatter/inline tags
|
||||
filterTags: string[]; // tags shown as filter pills (empty = auto-detect from frontmatter)
|
||||
}
|
||||
|
||||
interface DisplaySettings {
|
||||
rowSize: number; // px, base height of bookmark items (18–40)
|
||||
rowSpacing: number; // px, gap between items (0–12)
|
||||
indentSize: number; // px, indent per depth level (8–32)
|
||||
fontSize: number; // px, font size for bookmark labels (10–18)
|
||||
iconSize: number; // px, icon size (12–24)
|
||||
calendarCellSize: number; // px, calendar day cell height (20–48)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -94,10 +106,11 @@ interface BookmarkItem {
|
||||
|
||||
interface WaypointData {
|
||||
bookmarks: BookmarkItem[];
|
||||
recentFiles: { path: string; basename: string }[];
|
||||
}
|
||||
```
|
||||
|
||||
The bookmark tree is stored flat in `data.json` under `waypointData.bookmarks` but rendered recursively. Groups contain children; files/separators/spacers are leaf nodes.
|
||||
The bookmark tree is stored under `waypointData.bookmarks` and rendered recursively. Groups contain children; files/separators/spacers are leaf nodes. `waypointData.recentFiles` holds the persisted recent-files list (mirrored into the live `plugin.recentFiles` array on load).
|
||||
|
||||
---
|
||||
|
||||
@@ -121,15 +134,9 @@ All call `openPeriodNote(period, moment())` — opens or creates the current per
|
||||
|
||||
These call `navigatePeriodNote(direction)` which:
|
||||
1. Gets the active file's basename.
|
||||
2. Calls `detectPeriodType(basename)` — regex matching:
|
||||
- `^\d{4}-\d{2}-\d{2}$` → daily
|
||||
- `^\d{4}-W\d{2}$` → weekly
|
||||
- `^\d{4}-\d{2}$` → monthly
|
||||
- `^\d{4}-Q[1-4]$` → quarterly
|
||||
- `^\d{4}$` → yearly
|
||||
3. Parses date via `moment(basename, format)`.
|
||||
4. Adds ±1 period (quarters add ±3 months).
|
||||
5. Calls `openPeriodNote` for the new date.
|
||||
2. Calls `detectPeriodType(basename)` — **settings-driven**, not hardcoded regexes. It reads `settings.{daily,weekly,monthly,quarterly,yearly}.nameFormat` and tries `moment(basename, nameFormat, true).isValid()` (strict parsing) in order day → week → month → quarter → year, returning the first match as `{ period, date }`. Periods with an empty `nameFormat` are skipped; returns `null` if nothing matches. Changing a name format in settings therefore keeps next/prev navigation working.
|
||||
3. Adds ±1 period (quarters add ±3 months).
|
||||
4. Calls `openPeriodNote` for the new date.
|
||||
|
||||
### Other commands
|
||||
|
||||
@@ -142,7 +149,15 @@ These call `navigatePeriodNote(direction)` which:
|
||||
|
||||
## Period Note Creation (`openPeriodNote`)
|
||||
|
||||
For a given period + moment date:
|
||||
```typescript
|
||||
openPeriodNote(
|
||||
period: 'day' | 'week' | 'month' | 'quarter' | 'year',
|
||||
date: moment.Moment,
|
||||
leaf?: WorkspaceLeaf,
|
||||
): Promise<void>
|
||||
```
|
||||
|
||||
Single unified opener — there is no separate `openPeriodNoteInLeaf`. For a given period + moment date:
|
||||
|
||||
1. **Build filename:** `date.format(periodSettings.nameFormat) + ".md"`.
|
||||
2. **Build full path:** If `periodSettings.folder` is set, prepend it; otherwise root.
|
||||
@@ -151,7 +166,9 @@ For a given period + moment date:
|
||||
- Try to read template at `periodSettings.templateFile + ".md"`.
|
||||
- If template exists → `vault.create(fullPath, templateContent)`.
|
||||
- If no template → `vault.create(fullPath, minimalFrontmatter)` where frontmatter is `type: {typeProperty}` + `date: YYYY-MM-DD`.
|
||||
5. **Open in new leaf:** `workspace.getLeaf(false).openFile(file)`.
|
||||
5. **Open:** in `leaf` when one is supplied, otherwise in `workspace.getLeaf(false)`.
|
||||
|
||||
Middle-click handlers in the view pass an explicit tab leaf: `openPeriodNote(period, date, this.app.workspace.getLeaf('tab'))`.
|
||||
|
||||
---
|
||||
|
||||
@@ -159,32 +176,42 @@ For a given period + moment date:
|
||||
|
||||
### `file-open`
|
||||
|
||||
If `settings.recentFiles.updateOn === 'file-open'` → calls `addToRecentFiles(file)`.
|
||||
Tracks the opened file into recent files **only** when `settings.recentFiles.updateOn === 'file-open'`.
|
||||
|
||||
If `updateOn === 'file-edit'` → currently skipped (placeholder for future edit-detection).
|
||||
### `vault:modify`
|
||||
|
||||
Tracks the modified file (guarded to `TFile`) into recent files **only** when `updateOn === 'file-edit'`. Exactly one of the two modes is active at a time, so `'file-edit'` is a working mode rather than a no-op.
|
||||
|
||||
### `vault:create` / `vault:delete`
|
||||
|
||||
Both trigger `broadcastRedraw()` — tells all WaypointView instances to re-render (needed for calendar note indicators and stale recent file references).
|
||||
Both update the markdown-basename set used for calendar note indicators, then trigger `broadcastRedraw()` — telling all WaypointView instances to re-render (needed for the indicators and for stale recent file references).
|
||||
|
||||
### `vault:rename`
|
||||
|
||||
1. Updates `recentFiles` entry if path matches `oldPath`.
|
||||
2. Calls `updateBookmarkPath(oldPath, newPath)` — recursively scans `waypointData.bookmarks` and updates any `filePath` matching `oldPath`.
|
||||
1. Updates the markdown-basename set (old basename out, new basename in).
|
||||
2. Remaps the `recentFiles` entry through `remapRenamedPath(entry.path, oldPath, newPath)`.
|
||||
3. Remaps every bookmark `filePath` through the same helper.
|
||||
|
||||
`remapRenamedPath(path, oldPath, newPath)` (in `src/utils/path-utils.ts`) returns the updated path when `path` **is** `oldPath` (a plain file rename) *or* is nested under it (`path.startsWith(oldPath + '/')`, i.e. a folder rename/move), and `null` when the path is unaffected. This is why moving a folder no longer orphans the bookmarks inside it. The helper deliberately imports nothing from `'obsidian'` so it stays pure and unit-testable in plain node.
|
||||
|
||||
---
|
||||
|
||||
## Recent Files (in-memory)
|
||||
## Recent Files (persisted)
|
||||
|
||||
```typescript
|
||||
recentFiles: { path: string; basename: string }[]
|
||||
```
|
||||
|
||||
**Update flow:**
|
||||
1. `addToRecentFiles(file)` — dedupes (removes existing entry), prepends to front, truncates to `maxItems`.
|
||||
2. Calls `broadcastRedraw()`.
|
||||
Backed by `waypointData.recentFiles` in `data.json`, so the list survives vault reload. `loadWaypointData()` restores it and re-applies the current `maxItems` limit (in case the setting shrank since the last save).
|
||||
|
||||
**Omitted paths/tags:** Settings store regex patterns but the current `addToRecentFiles` does **not** filter by them — these are only exposed in the settings UI. The filtering is not yet wired up in the add logic.
|
||||
**Update flow:**
|
||||
1. `addToRecentFiles(file)` — omission check, then dedupes (removes existing entry), prepends to front, truncates to `maxItems`.
|
||||
2. `persistRecentFiles()` — mirrors the array into `waypointData.recentFiles` and schedules a debounced (300ms) `saveWaypointData()`.
|
||||
3. Calls `broadcastRedraw()`.
|
||||
|
||||
**Omitted paths/tags:** Both filters are applied in `addToRecentFiles`. `omittedPaths` entries are treated as regexes tested against `file.path`; `omittedTags` entries are regexes tested against the file's tags, read via `getAllTags(metadataCache.getFileCache(file))` with the leading `#` stripped. An invalid regex is skipped rather than throwing.
|
||||
|
||||
**`enforceRecentFilesLimit()`:** Called from the settings-change callback — trims the list when `maxItems` is lowered and persists the result.
|
||||
|
||||
**On file not found:** When clicking a recent file that no longer exists, `focusFile()` shows a Notice and removes the stale entry from `recentFiles`.
|
||||
|
||||
@@ -222,8 +249,8 @@ The last section (`waypoint-section:last-child`) gets `margin-top: auto`, pushin
|
||||
|
||||
- **Breadcrumb:** Q-label, month name, year — each clickable to open that period note.
|
||||
- **Nav buttons:** ◀/▶ shift month ±1. "Today" resets to current month.
|
||||
- **Week number column:** Clicking a week number opens the weekly note for that week's Monday.
|
||||
- **Day cells:** Click opens daily note. `.other-month` dimmed. `.today` has accent border. `.has-note` gets a dot indicator (checked via `vault.getFiles().some(f => f.basename === dateStr)`).
|
||||
- **Week number column:** Clicking a week number opens the weekly note for that week's Monday; middle-clicking opens it in a new tab.
|
||||
- **Day cells:** Click opens daily note, middle-click opens it in a new tab. `.other-month` dimmed. `.today` has accent border. `.has-note` gets a dot indicator, decided by the synchronous O(1) `plugin.hasNoteForDate(dateStr)` (a `Set` lookup — no per-cell vault scan).
|
||||
- **Grid generation:** `getMonthGrid(year, month, firstDayOfWeek)` in `date-utils.ts` produces up to 6 weeks, each with 7 `CalendarDay` objects containing `moment`, `dayOfMonth`, `isToday`, `isCurrentMonth`, `isoWeekNumber`.
|
||||
|
||||
### Recent Files Panel
|
||||
@@ -232,7 +259,7 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi
|
||||
|
||||
**Per-file features:**
|
||||
- **Active indicator:** `.is-active` class if path matches `workspace.getActiveFile()`.
|
||||
- **Remove button:** × icon, appears on hover (`.waypoint-recent-remove`), removes entry from array + saves settings + redraws.
|
||||
- **Remove button:** × icon, appears on hover (`.waypoint-recent-remove`), drops the entry from `plugin.recentFiles`, calls `persistRecentFiles()`, then redraws.
|
||||
- **Drag:** Uses `app.dragManager.dragFile()` for native Obsidian drag.
|
||||
- **Hover preview:** Triggers `hover-link` event for Obsidian's page preview popup.
|
||||
- **Context menu:** "Open in new tab" + Obsidian's native `file-menu` event.
|
||||
@@ -280,7 +307,7 @@ Renders `plugin.recentFiles` as Obsidian-native `nav-file` elements using the fi
|
||||
**Icon picker (`IconSuggestModal`):** Modal with:
|
||||
- Live preview of selected icon.
|
||||
- Search input with 60ms debounce.
|
||||
- Grid of matching icons (max 80 shown), loaded from `https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json` with fallback to `https://lucide.dev/api/tags` and a hardcoded `FALLBACK_ICONS` object (~300 icons).
|
||||
- Grid of matching icons (max 80 shown), loaded from `https://cdn.jsdelivr.net/npm/lucide-static@0.517.0/tags.json` with fallback to `https://lucide.dev/api/tags` and a hardcoded `FALLBACK_ICONS` object (~300 icons). The fetch result is cached for the session, so at most one network round-trip happens no matter how often the picker is opened.
|
||||
- Click to select, "No icon" link to clear, Save/Cancel buttons.
|
||||
|
||||
---
|
||||
@@ -345,7 +372,7 @@ Settings and waypoint data share a single `data.json` via Obsidian's `Plugin.loa
|
||||
}
|
||||
```
|
||||
|
||||
Both `loadSettings()` and `loadWaypointData()` read from the same file, merging partials over defaults. `saveSettings()` and `saveWaypointData()` each re-read the full data, update their key, and write back. This means they must not run concurrently (no locking — relies on Obsidian's synchronous save behavior).
|
||||
Both `loadSettings()` and `loadWaypointData()` read from the same file, merging partials over defaults. `saveSettings()` and `saveWaypointData()` each re-read the full data, update their key, and write back — so they are serialized through a single-writer save queue: each call chains onto the previous one's promise instead of racing it. Recent-file writes additionally go through the 300ms debounce in `persistRecentFiles()`.
|
||||
|
||||
---
|
||||
|
||||
@@ -353,7 +380,7 @@ Both `loadSettings()` and `loadWaypointData()` read from the same file, merging
|
||||
|
||||
`broadcastRedraw()` iterates all leaves of `WAYPOINT_VIEW_TYPE` and calls `view.redraw()` on each. `redraw()` is a full DOM rebuild (no virtual DOM, no diffing). This is triggered by:
|
||||
|
||||
- File open/create/delete/rename events
|
||||
- File open/create/delete/rename/modify events
|
||||
- Bookmark add/remove/update
|
||||
- Period navigation
|
||||
- Settings changes (via `onSettingsChange` callback)
|
||||
@@ -365,11 +392,5 @@ The sidebar is re-rendered from scratch on every change. For a small sidebar thi
|
||||
|
||||
## Known Limitations / Gaps
|
||||
|
||||
1. **Recent files omittedPaths/omittedTags** — Settings UI exposes these regex filters, but `addToRecentFiles()` does not apply them. Files are never filtered out.
|
||||
2. **`updateOn: 'file-edit'`** — The `file-open` handler returns early for this mode, but no edit-detection event is registered. Recent files never update in edit mode.
|
||||
3. **No virtual DOM** — Full rebuild on every redraw. Bookmark drag/drop, rename, and any change tears down and rebuilds the entire sidebar.
|
||||
4. **Concurrent saves** — `saveSettings()` and `saveWaypointData()` both re-read then write `data.json`. Rapid sequential calls could race.
|
||||
5. **Recent files not persisted** — Cleared on vault reload. Only bookmarks survive restarts.
|
||||
6. **Note indicators scan all vault files** — `hasNoteForDate()` iterates `vault.getFiles()` on every calendar render (called per day cell). For large vaults, this is O(days × files). Not cached.
|
||||
7. **`detectPeriodType` uses hardcoded formats** — The regex patterns in `main.ts` don't read from `settings.*.nameFormat`. If a user changes the name format to something non-standard, next/prev navigation breaks.
|
||||
8. **IconSuggestModal fetches from CDN** — Network dependency on first icon picker open. Falls back to hardcoded list on failure.
|
||||
1. **No virtual DOM** — Full rebuild on every redraw. Bookmark drag/drop, rename, and any change tears down and rebuilds the entire sidebar. For a small sidebar this is acceptable; for larger bookmark lists it may cause flicker.
|
||||
2. **IconSuggestModal fetches from CDN** — Network dependency for the Lucide tag list. The fetch happens at most once per session (the result is cached) and falls back to the hardcoded `FALLBACK_ICONS` list on failure, but a first-open with no network still degrades to that reduced list.
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
- [ ] Type "arrow" → should find all arrow icons
|
||||
- [ ] Type a tag keyword like "fitness" → should find `activity`, `dumbbell`, etc.
|
||||
- [ ] Clear search (empty input) → shows first 80 icons
|
||||
- [ ] Click "No icon" link → clears the icon
|
||||
- [ ] Set an icon on a bookmark, reopen the picker, click "No icon" → the previously-set icon is cleared
|
||||
- [ ] Click an icon → preview updates, grid highlights selection
|
||||
- [ ] Click Save → icon applied, click Cancel → no change
|
||||
- [ ] Test with **network offline** → falls back to FALLBACK_ICONS list
|
||||
@@ -56,12 +56,14 @@
|
||||
- [ ] Hover preview (page preview popup) still works on file bookmarks
|
||||
- [ ] Right-click context menu on a file bookmark still shows native Obsidian file-menu items
|
||||
- [ ] Bookmarks persist across vault reload
|
||||
- [ ] Move a folder containing bookmarked files to a new location → those bookmarks survive and still open the moved files
|
||||
|
||||
## Calendar (Regression)
|
||||
|
||||
- [ ] Calendar renders with correct month grid
|
||||
- [ ] Click on a day → opens/creates daily note
|
||||
- [ ] Click on week number → opens/creates weekly note
|
||||
- [ ] Middle-click on a week number → opens/creates the weekly note in a new tab, with no console error
|
||||
- [ ] Click Q/M/Y breadcrumb → opens respective period note
|
||||
- [ ] ◀/▶ navigation shifts month
|
||||
- [ ] "Today" button returns to current month
|
||||
|
||||
Reference in New Issue
Block a user