|
| 1 | +# Issue #144 — Open Previous / Open Next entry navigation |
| 2 | + |
| 3 | +> **Issue:** [pajoma/vscode-journal#144](https://github.com/pajoma/vscode-journal/issues/144) |
| 4 | +> **Branch:** `144-journal-open-previous-and-open-next-feature` |
| 5 | +> **Created:** 2026-05-14 |
| 6 | +
|
| 7 | +## Goal |
| 8 | + |
| 9 | +Add two commands — `journal.openPrevious` and `journal.openNext` — that step backwards / forwards through journal **daily entries** relative to the currently open file. Step semantics are user-configurable: either "next/previous **existing** entry on disk" (default) or "next/previous **calendar day** (create if missing)". Navigation honors the active journal scope. Default keybindings: `Ctrl+J ,` (previous) and `Ctrl+J .` (next). |
| 10 | + |
| 11 | +## Why now |
| 12 | + |
| 13 | +Filed in 2018 by a user with parallel work/personal journals. Pain point: traversing meeting notes day-by-day requires opening the smart-input prompt and typing offsets each time. The pain compounds with multi-day gaps (weekends, holidays) where `-1` doesn't land on an existing entry. Now is a reasonable time because: |
| 14 | + |
| 15 | +- The `ScanEntries` feature (added later for the picklist) already enumerates all entries on disk — `openPrevious` mode `existing` can reuse that walk. |
| 16 | +- `getDateFromURIAndConfig` (`src/util/paths.ts:113`) already extracts a `Date` from a journal entry's path — needed to identify the "current" anchor. |
| 17 | +- `1.1.0` milestone owns this issue and pruning the milestone is a release goal. |
| 18 | + |
| 19 | +## Scope |
| 20 | + |
| 21 | +### In scope |
| 22 | + |
| 23 | +1. **Two new commands** registered under `journal.openPrevious` and `journal.openNext` with command palette titles and l10n entries across the eleven existing locales (en, de, fr, es, it, pt, nl, ru, zh, ja, ar). |
| 24 | + |
| 25 | +2. **Two navigation modes**, selected by a new setting `journal.navigation.mode`: |
| 26 | + - `"existing"` *(default)* — find the previous/next entry whose file already exists on disk. Skip gaps. If no prior entry exists, show an info toast ("No earlier journal entry found") and do nothing. |
| 27 | + - `"calendar"` — step exactly `-1` / `+1` calendar day from the anchor. Create the target entry if missing (same code path as `Open Yesterday` / `Open Tomorrow` already exercise). No skipping. |
| 28 | + |
| 29 | +3. **Anchor resolution**: |
| 30 | + - If `vscode.window.activeTextEditor.document.uri` is a journal **daily entry** (date parseable via `getDateFromURIAndConfig`), that entry's date is the anchor. |
| 31 | + - Otherwise (no editor open, or the active document is not a journal entry — e.g. a note, weekly entry, attachment, or some unrelated file), the anchor is **today**. |
| 32 | + - When the anchor is today and mode is `existing`, "previous" means the most recent past entry; "next" means today (if exists) or the nearest future entry (if any was pre-created). |
| 33 | + |
| 34 | +4. **Scope honoring**: |
| 35 | + - The active scope is derived from the anchor entry's path (which scope's `${base}` matches the parent directory). If the anchor is "today" (no file open), the active scope is the **default** scope. |
| 36 | + - Navigation in `existing` mode walks only the entries under the active scope's resolved entry directory. Cross-scope navigation is not in scope. |
| 37 | + |
| 38 | +5. **Default keybindings**: `Ctrl+J ,` → `journal.openPrevious`, `Ctrl+J .` → `journal.openNext`. Both bound to the `editorTextFocus` `when` clause (consistent with `journal.printDuration` / `journal.printSum`). User can override via `Keyboard Shortcuts`. |
| 39 | + |
| 40 | +6. **Settings UI**: register `journal.navigation.mode` in `package.json` `contributes.configuration` with enum values `"existing"` and `"calendar"` and a default of `"existing"`. Translated description strings. |
| 41 | + |
| 42 | +7. **Tests**: |
| 43 | + - Unit-level coverage for the date-stepping helper(s) used to resolve "previous existing" and "next existing" against a mocked directory listing. |
| 44 | + - Integration test in the Extension Host: pre-seed a tmp workspace with three entries (e.g. `2025-03-05`, `2025-03-08`, `2025-03-12`), open the middle one, run `openPrevious`, assert the `2025-03-05` entry is opened. Same shape for `openNext`. Same shape for `calendar` mode with create-if-missing. |
| 45 | + - Regression test: when no anchor file is open, `openPrevious` walks back from today. |
| 46 | + - Regression test: when active scope is non-default, navigation stays within that scope's directory. |
| 47 | + |
| 48 | +### Out of scope |
| 49 | + |
| 50 | +- **Weekly entries.** `${base}/<year>/<week>.md` (or wherever weeklies live) is not part of the chronological traversal in this PR. They can be added later if requested. (Issue text mentions "notes" only.) |
| 51 | +- **Notes** (the per-day subdirectory note files). Navigating across note files within a day or across days is a separate, more complex problem (mixing chronology with title-based ordering). Out of scope per the clarifying-question answers — user chose "honor active scope" without selecting notes. |
| 52 | +- **Cross-scope navigation.** If the user has both `work` and `private` scopes, navigation does not jump from `work` entries to `private` entries. |
| 53 | +- **Wrap-around.** Reaching the oldest/newest entry shows an info toast and stops. No wrap-around to opposite end. |
| 54 | +- **Recent-files MRU traversal.** Navigation is by entry **date**, not by file-modification time. |
| 55 | +- **CodeLens / status-bar navigation buttons.** Command palette + keybindings only. |
| 56 | +- **Caching.** `ScanEntries` already maintains a cache for the picklist; reuse it if convenient, but a dedicated cache for prev/next is not in scope. A fresh `vscode.workspace.fs.readDirectory` walk per invocation is acceptable for typical journal sizes. |
| 57 | + |
| 58 | +## Acceptance criteria |
| 59 | + |
| 60 | +1. `npm run check` clean. New tests pass on first run. |
| 61 | +2. **Manual — `existing` mode (default):** |
| 62 | + - Workspace pre-seeded with daily entries on `2025-03-05`, `2025-03-08`, `2025-03-12`. No entry for any other date. |
| 63 | + - Open `2025-03-08`. `Ctrl+J ,` opens `2025-03-05`. `Ctrl+J .` opens `2025-03-12`. |
| 64 | + - From `2025-03-12`, `Ctrl+J .` shows the info toast "No later journal entry found." and the editor does not change. |
| 65 | + - From `2025-03-05`, `Ctrl+J ,` shows "No earlier journal entry found." and the editor does not change. |
| 66 | + - No `[ERROR]` lines in the Journal output channel. |
| 67 | +3. **Manual — `calendar` mode:** |
| 68 | + - With `journal.navigation.mode` set to `calendar`, open `2025-03-08`. `Ctrl+J ,` opens `2025-03-07` (created if missing). `Ctrl+J .` opens `2025-03-09` (created if missing). |
| 69 | +4. **Manual — no anchor:** |
| 70 | + - Close all editors. `Ctrl+J ,` (in `existing` mode) opens the most recent past entry (e.g. `2025-03-12` from the seed above if today is later). `Ctrl+J .` opens the nearest future entry or shows the toast if none. |
| 71 | +5. **Manual — multi-scope:** |
| 72 | + - Two scopes configured: `default` and `work`. Seed `default` with entries `2025-03-05/08/12` and `work` with entries `2025-04-01/02`. Open the `work` entry `2025-04-02`. `Ctrl+J ,` opens `2025-04-01`, NOT a default-scope entry. |
| 73 | +6. **L10n:** Command palette titles and the "No earlier/later entry found" toast render in German, French, Spanish, etc. — at least one non-English locale spot-checked. |
| 74 | + |
| 75 | +## Entities / contracts touched |
| 76 | + |
| 77 | +- `src/provider/commands/` — two new command files: |
| 78 | + - `open-previous-entry.ts` (`journal.openPrevious`) |
| 79 | + - `open-next-entry.ts` (`journal.openNext`) |
| 80 | + Each follows the existing static-`create(ctrl)` pattern (see `show-entry-for-today.ts`). |
| 81 | +- `src/provider/commands/index.ts` — register the new commands. |
| 82 | +- `src/ext/startup.ts` — wire the new commands into the activation sequence. |
| 83 | +- `src/actions/navigation.ts` — **new file** containing the pure navigation logic: |
| 84 | + - `resolveAnchor(ctrl, activeEditor): Promise<{ date: Date; scope: string }>` |
| 85 | + - `findAdjacentEntry(ctrl, anchor: { date; scope }, direction: 'previous' | 'next', mode: 'existing' | 'calendar'): Promise<Date | null>` |
| 86 | + Separating logic from command surface lets tests target the helpers directly without command-palette plumbing. |
| 87 | +- `package.json`: |
| 88 | + - `contributes.commands` — two new entries. |
| 89 | + - `contributes.keybindings` — two new entries (`ctrl+j ,` and `ctrl+j .`, `when: editorTextFocus`). |
| 90 | + - `contributes.configuration.properties` — new `journal.navigation.mode` enum. |
| 91 | +- `package.nls.json` and `package.nls.<locale>.json` for all eleven locales — new strings: |
| 92 | + - `command.journal.openPrevious.title` |
| 93 | + - `command.journal.openNext.title` |
| 94 | + - `configuration.journal.navigation.mode.description` |
| 95 | +- `l10n/bundle.l10n.json` and `l10n/bundle.l10n.<locale>.json` — runtime strings for the toasts ("No earlier journal entry found", "No later journal entry found"). |
| 96 | +- `src/test/suite/` — new test file `commands-prev-next.test.ts` (matches the `commands-*` naming pattern). |
| 97 | + |
| 98 | +## Constraints |
| 99 | + |
| 100 | +- **No raw `fs`.** All FS access via `vscode.workspace.fs` (PLAN.md Phase 1.3 invariant). |
| 101 | +- **`vscode.l10n`.** New user-facing strings go through `vscode.l10n.t(...)` for runtime, NLS keys for manifest. |
| 102 | +- **Anchor detection must be robust** to paths that look journal-shaped but are not (e.g. a markdown file in the journal base that wasn't created by this extension). The anchor resolution falls back to "today" on any parse failure rather than throwing. |
| 103 | +- **Performance.** For workspaces with thousands of entries, the `existing` walk should not block the UI noticeably. Strategy: walk the entry directory (already structured by year/month per `journal.patterns.notes.path` defaults), short-circuit as soon as the adjacent file is found rather than enumerating the entire tree. |
| 104 | +- **No promise wrappers in new code.** Write native `async/await` from the start (PLAN.md Phase 2.2 direction). |
| 105 | +- **Named imports preferred.** New files use `import { Foo } from '...'` rather than `import * as J from '...'` for new code (PLAN.md Phase 2.3 direction). Existing wiring code can stay namespace-style. |
| 106 | + |
| 107 | +## Open questions |
| 108 | + |
| 109 | +1. **Year-boundary traversal in `existing` mode.** If the entry directory is `${base}/2024/12/31.md` and the next entry is `${base}/2025/01/05.md`, the walk must cross the year subdirectory. The plan must spell out the directory enumeration order to ensure this works. |
| 110 | +2. **What counts as a "daily entry" in the walk?** The entry filename pattern defaults to `${day}.${ext}` (e.g. `14.md`). Notes live in `${base}/<year>/<month>/<day>/<title>.md`. Anchor detection must distinguish — likely by matching the exact path template, not just "any markdown file under base". |
| 111 | +3. **Stale `ScanEntries` cache.** If the cache is used and a new entry has been created since the cache was populated, the cache may miss it. Plan: either bypass the cache for navigation, or invalidate after `createEntryForPath`. Decision deferred to the plan. |
| 112 | + |
| 113 | +## Related issues |
| 114 | + |
| 115 | +- Indirectly related: `ScanEntries` and the picklist feature share directory-walking logic — reuse vs. duplicate is a plan-level decision. |
| 116 | +- No cross-repo dependencies. No `blocked-by:` / `blocks:` labels. |
| 117 | + |
| 118 | +## Reference |
| 119 | + |
| 120 | +- Issue body (one paragraph): user navigates meeting notes day-by-day, wants prev/next shortcuts that complement `Open Yesterday` / `Open Tomorrow`. |
| 121 | +- Existing similar surfaces for inspiration: |
| 122 | + - `src/provider/commands/show-entry-for-date.ts:97` — already does anchor-aware entry resolution for the smart-input. |
| 123 | + - `src/provider/features/scan-entries.ts:13` — directory-walking + caching. |
| 124 | + - `src/util/paths.ts:113` — `getDateFromURIAndConfig` for anchor parsing. |
0 commit comments