Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
npm install # Install dependencies
npm run check # TypeScript type checking (no emit)
npm run build # Production build → build/main.js, then copies to live vault
npm run zip # Package main.js + manifest.json into a zip for release
eslint main.ts # Lint a file (requires global eslint: npm install -g eslint)
eslint ./lib/ # Lint all files in a folder
```

`npm run build` runs esbuild then `npm run copy`, which copies `build/main.js` and `manifest.json` directly into the developer's live Obsidian vault for manual testing. There is no automated test suite.

## Architecture

This is an **Obsidian community plugin** that enables sequential note navigation using the `previous` YAML frontmatter property and backlink relationships, with optional implicit navigation for periodic (daily/weekly) notes.

**Source files:**

- `main.ts` — Plugin entry point. Registers 8 commands and the settings tab on `onload`. Should remain minimal.
- `lib/commands.ts` — Command implementations: `go-to-previous-note`, `go-to-next-note`, `go-to-first-note`, `go-to-last-note`, `detach-note`, `insert-note`, `insert-note-to-first`, `insert-note-to-last`.
- `lib/obsidian.ts` — All core logic: `getPreviousNote()`, `getNextNotes()`, `findFirstNote()`, `findLastNote()`, `detachNote()`, `setPreviousProperty()`. Also contains periodic note helpers.
- `lib/settings.ts` — `MyPluginSettings` interface, `DEFAULT_SETTINGS`, and `MySettingTab` (the plugin's settings UI). Settings cover daily and weekly note navigation (enable toggle, date format, folder path).
- `lib/NextNoteSuggestModal.ts` — Extends `SuggestModal`. Used both when navigating to a branching next note and when picking a target note for insert commands.
- `lib/ConfirmModal.ts` — Simple confirmation dialog, used by `detach-note`.
- `lib/utils.ts` — `extractLinktext()` parses wiki-links; `formatFolderPath()` normalises folder path input from settings.

**Navigation logic:**

- *Previous/Next*: follow `previous` frontmatter wikilink, or implicitly navigate between periodic notes if enabled and the file matches the configured format+folder.
- *First/Last*: traverse the full chain with loop detection; prompts via modal when a branch is encountered.
- *Detach*: removes the current note from the chain by re-linking its predecessor and successor(s) directly, then clears its own `previous` property.
- *Insert*: positions the current note at an arbitrary point in a chain by detaching it first, then re-wiring the surrounding notes.
- `"ROOT"` is the sentinel value written to `previous` when a note becomes the first in a chain with no predecessor.

**Obsidian API patterns used:** `app.metadataCache` for frontmatter and backlink resolution (`resolvedLinks`); `app.fileManager.processFrontMatter()` for safe frontmatter writes; `moment` (re-exported by Obsidian) for date parsing in periodic note logic.

## Key constraints

- Bundle target is `es2020` CJS via esbuild; the `obsidian` package is external (never bundled).
- `isDesktopOnly: false` — avoid Node/Electron-only APIs.
- Command IDs must never be renamed after release (users may have hotkeys bound to them).
- Use `this.register*` helpers (`registerEvent`, `registerDomEvent`, `registerInterval`) for all listeners so they are cleaned up on unload.
- Versioning: bump `version` in `manifest.json` (SemVer, no leading `v`), update `versions.json`, then create a matching GitHub release with `main.js` and `manifest.json` as assets.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ An Obsidian plugin that enables navigation between notes using the `previous` pr
## Features

### Go to previous note

Jump to the note specified in the `previous` property of the current note's frontmatter.

### Go to next note

Move to notes that backlink to the current note and have their `previous` property pointing to it.
If multiple candidates exist, a suggestion modal will allow you to choose.

### Daily/ weekly note navigation

Daily notes and weekly notes are considered to be in sequence even if they don't have `previous` property, so You can navigate to previous daily note, next daily note, previous weekly note and next weekly note.
You can configure the daily notes and weekly notes settings, such as disable this feature, or set the file format of the daily note.

### Go to first note

Follow the `previous` property chain to reach the first note in the sequence.

### Go to last note

Use backlinks to find the last note in the sequence.
If there are multiple candidates, a suggestion modal will appear for selection.

Expand All @@ -29,4 +38,4 @@ If there are multiple candidates, a suggestion modal will appear for selection.

## Contributing

Feel free to submit bug reports and feature requests via Issues. Contributions through pull requests are also highly appreciated!
Feel free to submit bug reports and feature requests via Issues. Contributions through pull requests are also highly appreciated!
39 changes: 20 additions & 19 deletions lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ import { App, TFile, Notice } from "obsidian";
import { ConfirmModal } from "./ConfirmModal";
import { NextNoteSuggestModal } from "./NextNoteSuggestModal";
import { getActiveFile, getPreviousNote, getNextNotes, detachNote, setPreviousProperty, findLastNote, findFirstNote } from "./obsidian";
import { MyPluginSettings } from "./settings";

export async function goToPreviousNoteCommand(app: App) {
export async function goToPreviousNoteCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
}

const target = getPreviousNote(app, file);
const target = getPreviousNote(app, file, settings);
if (!target) {
return;
}

await app.workspace.getLeaf().openFile(target);
}

export async function goToNextNoteCommand(app: App) {
export async function goToNextNoteCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
}

const nextNotes = getNextNotes(app, file);
const nextNotes = getNextNotes(app, file, settings);

if (nextNotes.length === 0) {
return;
Expand All @@ -40,31 +41,31 @@ export async function goToNextNoteCommand(app: App) {
}
}

export async function goToFirstNoteCommand(app: App) {
export async function goToFirstNoteCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
}

const firstNote = await findFirstNote(app, file);
const firstNote = await findFirstNote(app, file, settings);
if (firstNote !== file) {
await app.workspace.getLeaf().openFile(firstNote);
}
}

export async function goToLastNoteCommand(app: App) {
export async function goToLastNoteCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
}

const lastNote = await findLastNote(app, file);
const lastNote = await findLastNote(app, file, settings);
if (lastNote && lastNote !== file) {
await app.workspace.getLeaf().openFile(lastNote);
}
}

export async function detachNoteCommand(app: App) {
export async function detachNoteCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
Expand All @@ -75,12 +76,12 @@ export async function detachNoteCommand(app: App) {
"Detach Note",
`Are you sure you want to detach "${file.basename}" from the chain?`,
async () => {
await detachNote(app, file, { showNotification: true });
await detachNote(app, file, settings, { showNotification: true });
}
).open();
}

export async function insertNoteToLastCommand(app: App) {
export async function insertNoteToLastCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
Expand All @@ -94,9 +95,9 @@ export async function insertNoteToLastCommand(app: App) {
return;
}

await detachNote(app, file);
await detachNote(app, file, settings);

const lastNote = await findLastNote(app, selectedNote);
const lastNote = await findLastNote(app, selectedNote, settings);
if (!lastNote) {
return;
}
Expand All @@ -105,7 +106,7 @@ export async function insertNoteToLastCommand(app: App) {
new Notice(`Inserted note to last: ${lastNote.basename}`);
}

export async function insertNoteCommand(app: App) {
export async function insertNoteCommand(app: App, settigns: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
Expand All @@ -122,10 +123,10 @@ export async function insertNoteCommand(app: App) {
}

// 2. Detach current note
await detachNote(app, file);
await detachNote(app, file, settigns);

// 3. Find successors of the target note (notes that currently point to target)
const successors = getNextNotes(app, selectedNote);
const successors = getNextNotes(app, selectedNote, settigns);

// 4. Link current note to target
await setPreviousProperty(app, file, selectedNote.basename);
Expand All @@ -141,7 +142,7 @@ export async function insertNoteCommand(app: App) {
}
}

export async function insertNoteToFirstCommand(app: App) {
export async function insertNoteToFirstCommand(app: App, settings: MyPluginSettings) {
const file = getActiveFile(app);
if (!file) {
return;
Expand All @@ -157,10 +158,10 @@ export async function insertNoteToFirstCommand(app: App) {
}

// 2. Detach current note
await detachNote(app, file);
await detachNote(app, file, settings);

// 3. Find first note of the chain
const firstNote = await findFirstNote(app, selectedNote);
const firstNote = await findFirstNote(app, selectedNote, settings);

// 4. Update first note to point to current note
await setPreviousProperty(app, firstNote, file.basename);
Expand Down
Loading