Skip to content

Commit 2f4e8a3

Browse files
authored
Merge pull request #182 from pajoma/144-journal-open-previous-and-open-next-feature
feat(navigation): open previous / next entry commands (#144)
2 parents facc300 + 7bdf100 commit 2f4e8a3

34 files changed

Lines changed: 1047 additions & 25 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
## Unreleased
1313

14+
### Added
15+
* [Issue #144](https://github.com/pajoma/vscode-journal/issues/144) New commands `journal.openPrevious` (`ctrl+j ,` / `cmd+j ,`) and `journal.openNext` (`ctrl+j .` / `cmd+j .`) step backwards / forwards through journal entries relative to the file currently in focus. New setting `journal.navigation.mode` chooses between `existing` (default — skip gaps to the next entry on disk) and `calendar` (step exactly one day, create if missing). Navigation honors the active scope.
16+
1417
### Fixed
1518
* [Issue #51](https://github.com/pajoma/vscode-journal/issues/51) Remote SSH and WSL Remote no longer surface a "File not found" error toast on first-time creation of an entry, weekly page, or note. Replaced the open-before-create antipattern in `Reader.loadEntryForDay`, `Reader.loadEntryForWeek`, and `LoadNotes.loadNote` with a stat-first check (new `fileExists` helper in `src/util/fs-exists.ts`) and migrated the three methods to native `async/await`.
1619

docs/commands.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ You can access all functionality (besides opening the journal) from the smart in
55

66
## Journal Pages
77

8-
* `journal:day` (keybindings: `ctrl+shift+j` or `cmd+shift+j` on mac) opens the smart input, see
8+
* `journal:day` (keybindings: `ctrl+shift+j` or `cmd+shift+j` on mac) opens the smart input, see
99
* `journal:today` for opening today's entry
1010
* `journal:tomorrow`
11+
* `journal:openPrevious` (keybindings: `ctrl+j ,` or `cmd+j ,` on mac) opens the previous journal entry relative to the file currently in focus. When no journal file is open, navigation starts from today. Behavior controlled by `journal.navigation.mode` (see `settings.md`):
12+
* `existing` *(default)* — skip gaps and open the previous entry that exists on disk. Shows an info toast at the start of history.
13+
* `calendar` — step exactly one day back and create the entry if missing.
14+
* `journal:openNext` (keybindings: `ctrl+j .` or `cmd+j .` on mac) mirror of `openPrevious` for forward navigation.
1115

1216
## Notes & Memos
13-
`journal:note` opens a dialog to enter the title of a new page for notes.
17+
`journal:note` opens a dialog to enter the title of a new page for notes.
1418

1519
## Open the journal
1620
`journal:open` starts a new instance of vscode with the base directory of your journal as root

docs/plans/2026-05-14-feat-144-prev-next-navigation.md

Lines changed: 271 additions & 0 deletions
Large diffs are not rendered by default.

docs/settings.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ Depending on the version this setting might activate certain new features. In ge
215215

216216
Controls if new files are created in full mode or in a new editor group (split pane).
217217

218+
### Navigation Mode
219+
* Key: `journal.navigation.mode`
220+
* Default value: `existing`
221+
* Allowed values: `existing`, `calendar`
218222

223+
Controls how the `Open Previous Journal Entry` (`ctrl+j ,`) and `Open Next Journal Entry` (`ctrl+j .`) commands step through entries.
224+
225+
| Value | Behavior |
226+
|-------|----------|
227+
| `existing` *(default)* | Find the previous / next entry that already exists on disk. Skips gaps (weekends, vacations). At the start or end of history an info toast is shown and the editor does not change. |
228+
| `calendar` | Step exactly one calendar day back or forward from the anchor file. Creates the target entry if missing (same as `Open Yesterday` / `Open Tomorrow`). |
229+
230+
The anchor is the currently open journal entry. When no journal file is open, the anchor falls back to today. Navigation honors the active scope — derived from the anchor file's path or the default scope when no anchor file is open.
219231

220232
![Screen Capture](./set-base-directory.gif)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.

l10n/bundle.l10n.ar.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
"Add task to entry for week {week}": "إضافة مهمة للأسبوع {week}",
1414
"Add task to entry {day}": "إضافة مهمة إلى الإدخال {day}",
1515
"Add memo to entry {day}": "إضافة مذكرة إلى الإدخال {day}",
16-
"Create or open entry {day}": "إنشاء أو فتح الإدخال {day}"
16+
"Create or open entry {day}": "إنشاء أو فتح الإدخال {day}",
17+
"No earlier journal entry found.": "لم يتم العثور على إدخال مجلة سابق.",
18+
"No later journal entry found.": "لم يتم العثور على إدخال مجلة لاحق."
1719
}

l10n/bundle.l10n.de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
"Add task to entry for week {week}": "Aufgabe zum Eintrag für Woche {week} hinzufügen",
1414
"Add task to entry {day}": "Aufgabe zum Eintrag {day} hinzufügen",
1515
"Add memo to entry {day}": "Memo zum Eintrag {day} hinzufügen",
16-
"Create or open entry {day}": "Eintrag {day} erstellen oder öffnen"
16+
"Create or open entry {day}": "Eintrag {day} erstellen oder öffnen",
17+
"No earlier journal entry found.": "Kein früherer Journaleintrag gefunden.",
18+
"No later journal entry found.": "Kein späterer Journaleintrag gefunden."
1719
}

l10n/bundle.l10n.es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
"Add task to entry for week {week}": "Añadir tarea a la entrada de la semana {week}",
1414
"Add task to entry {day}": "Añadir tarea a la entrada del {day}",
1515
"Add memo to entry {day}": "Agregar un memo a la entrada {day}",
16-
"Create or open entry {day}": "Crear o abrir una entrada {day}"
16+
"Create or open entry {day}": "Crear o abrir una entrada {day}",
17+
"No earlier journal entry found.": "No se encontró ninguna entrada anterior del diario.",
18+
"No later journal entry found.": "No se encontró ninguna entrada posterior del diario."
1719
}

l10n/bundle.l10n.fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
"Add task to entry for week {week}": "Ajouter une tâche à l'entrée de la semaine {week}",
1414
"Add task to entry {day}": "Ajouter une tâche à l'entrée du {day}",
1515
"Add memo to entry {day}": "Ajouter un mémo à l'entrée {day}",
16-
"Create or open entry {day}": "Créer ou ouvrir une entrée {day}"
16+
"Create or open entry {day}": "Créer ou ouvrir une entrée {day}",
17+
"No earlier journal entry found.": "Aucune entrée antérieure du journal trouvée.",
18+
"No later journal entry found.": "Aucune entrée ultérieure du journal trouvée."
1719
}

l10n/bundle.l10n.it.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@
1313
"Add task to entry for week {week}": "Aggiungi un compito per la settimana {week}",
1414
"Add task to entry {day}": "Aggiungi un compito all'entrata {day}",
1515
"Add memo to entry {day}": "Aggiungi un memo all'entrata {day}",
16-
"Create or open entry {day}": "Crea o apri l'entrata {day}"
16+
"Create or open entry {day}": "Crea o apri l'entrata {day}",
17+
"No earlier journal entry found.": "Nessuna voce precedente del diario trovata.",
18+
"No later journal entry found.": "Nessuna voce successiva del diario trovata."
1719
}

0 commit comments

Comments
 (0)