diff --git a/README.md b/README.md
index 3d29501..be631ae 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,7 @@ it; in the document pane it is that document. In the browse pane:
| `T` | today's diary entry, started if there is none |
| `C` | the diary by month (`:cal`) — see [The diary](#the-diary) |
| `[d` / `]d` | the diary entry before / after the one open, by date |
-| `c` | capture a card into this space's inbox without leaving where you are — reading a document, a card about it |
+| `c` | capture a card into this space's inbox without leaving where you are — reading a document, a card about it; `Tab` on the line makes it a note, or a line in today's entry |
| `Q` | the scratchpad, lowered over whatever is on screen — see [The scratchpad](docs/wiki/Writing.md#the-scratchpad) |
| `Esc`, `q` | close the document, and its tile when there are several |
| `h` | up one level in the tree |
@@ -264,7 +264,8 @@ done cards older than a week fold away until `z` shows them.
`c` captures from anywhere: `:card
` outside the board puts the card in
this space's inbox (or the first space that has a board) and leaves you where
-you were. On the board, `o` or `c` puts a card on the column under the cursor.
+you were. `Tab` on that line makes it a note on the scratchpad instead, or a
+timed line in today's diary entry (`:today `). On the board, `o` or `c` puts a card on the column under the cursor.
| key | action |
| --- | --- |
@@ -375,7 +376,9 @@ named by `"space": ""`, or else of the first space that has a board,
the same as `:card` outside the board. The first line is the card's title and
the rest its notes. `"to": "diary"` adds `- 14:05 call the bank` to the end of
today's entry, in `WORKSPACE_TIMEZONE`, and starts the entry when there is
-none yet, from `templates/day` when the diary has one. The answer says where it went: `{"to":"card","id":7,"where":"work/backlog"}`.
+none yet, from `templates/day` when the diary has one. `"to": "note"` puts it
+on the scratchpad, and `"to": "document"` at the end of the document named by
+`"document"`, as a link names it: `Plan`, `[[Plan]]` or `/n/work/plan`. The answer says where it went: `{"to":"card","id":7,"where":"work/backlog"}`.
A token opens this one route and nothing else — it reads nothing, not even the
list of spaces — and only its SHA-256 is kept, the way a share key is. Wrong
@@ -875,7 +878,7 @@ sealed or not.
| `POST` | `/api/diary/day` | `{ day? }`: the entry of `2026-10-22`, or of today in `WORKSPACE_TIMEZONE`, started from `templates/day` when there is none; answers `{ id, created }` |
| `GET` | `/api/diary/around/{id}` | the entries before and after a diary entry, by date |
| `GET` | `/api/nodes/{id}/templates` | the templates of the space the node is in |
-| `POST` | `/api/capture` | `{ text, to: "card" \| "diary", space? }`, with a session or `Authorization: Bearer` a token |
+| `POST` | `/api/capture` | `{ text, to: "card" \| "diary" \| "note" \| "document", space?, document?, note? }`, with a session or `Authorization: Bearer` a token; `note` is a scratch note the text came from, dropped once it has landed |
| `GET`/`POST` | `/api/tokens` | the capture tokens / make one, `{ name }`, answered with the token this once |
| `DELETE` | `/api/tokens/{id}` | remove a capture token |
| `POST` | `/api/spaces/{id}/lists` | add a list |
@@ -884,7 +887,7 @@ sealed or not.
| `PUT`/`DELETE` | `/api/cards/{id}` | update (title, body, due, repeat, checklist, labels, done, link) / delete; finishing a repeating card answers its copy as `again` |
| `POST` | `/api/cards/{id}/move` | to another list and place in it; `again` as above |
| `GET` | `/api/cards?q=…` | cards whose title, summary or a checklist step matches, open ones first; `space` keeps it to one board |
-| `GET`/`POST` | `/api/scratch` | the scratchpad's notes, the one touched last first / a new note `{ body }` |
+| `GET`/`POST` | `/api/scratch` | the scratchpad's notes, the one touched last first, or with `?q=` those a search finds / a new note `{ body }` |
| `PUT`/`DELETE` | `/api/scratch/{id}` | rewrite a note `{ body }` / drop it |
| `POST` | `/api/nodes/{id}/move` | reparent, and place among the new siblings |
| `POST` | `/api/nodes/move` | `{ ids, parent_id }`: several into one folder, all or none |
diff --git a/docs/wiki/API.md b/docs/wiki/API.md
index 9b33a74..b38dfbc 100644
--- a/docs/wiki/API.md
+++ b/docs/wiki/API.md
@@ -25,7 +25,7 @@ sealed or not.
| `POST` | `/api/diary/day` | `{ day? }`: the entry of `2026-10-22`, or of today in `WORKSPACE_TIMEZONE`, started from `templates/day` when there is none; answers `{ id, created }` |
| `GET` | `/api/diary/around/{id}` | the entries before and after a diary entry, by date |
| `GET` | `/api/nodes/{id}/templates` | the templates of the space the node is in |
-| `POST` | `/api/capture` | `{ text, to: "card" \| "diary", space? }`, with a session or `Authorization: Bearer` a token |
+| `POST` | `/api/capture` | `{ text, to: "card" \| "diary" \| "note" \| "document", space?, document?, note? }`, with a session or `Authorization: Bearer` a token; `note` is a scratch note the text came from, dropped once it has landed |
| `GET`/`POST` | `/api/tokens` | the capture tokens / make one, `{ name }`, answered with the token this once |
| `DELETE` | `/api/tokens/{id}` | remove a capture token |
| `POST` | `/api/spaces/{id}/lists` | add a list |
@@ -34,7 +34,7 @@ sealed or not.
| `PUT`/`DELETE` | `/api/cards/{id}` | update (title, body, due, repeat, checklist, labels, done, link) / delete; finishing a repeating card answers its copy as `again` |
| `POST` | `/api/cards/{id}/move` | to another list and place in it; `again` as above |
| `GET` | `/api/cards?q=…` | cards whose title, summary or a checklist step matches, open ones first; `space` keeps it to one board |
-| `GET`/`POST` | `/api/scratch` | the scratchpad's notes, the one touched last first / a new note `{ body }` |
+| `GET`/`POST` | `/api/scratch` | the scratchpad's notes, the one touched last first, or with `?q=` those a search finds / a new note `{ body }` |
| `PUT`/`DELETE` | `/api/scratch/{id}` | rewrite a note `{ body }` / drop it |
| `POST` | `/api/nodes/{id}/move` | reparent, and place among the new siblings |
| `POST` | `/api/nodes/move` | `{ ids, parent_id }`: several into one folder, all or none |
diff --git a/docs/wiki/Capture.md b/docs/wiki/Capture.md
index 87995ee..532cf0c 100644
--- a/docs/wiki/Capture.md
+++ b/docs/wiki/Capture.md
@@ -27,14 +27,20 @@ curl -H "Authorization: Bearer nwc_…" -H 'content-type: application/json' \
| field | meaning |
| --- | --- |
| `text` | what to write down; for a card, the first line is the title and the rest its notes |
-| `to` | `"card"` (the default) or `"diary"` |
-| `space` | for a card, the slug of the space whose inbox it goes in |
+| `to` | `"card"` (the default), `"diary"`, `"note"` or `"document"` |
+| `space` | for a card, the slug of the space whose inbox it goes in; for a document, the space its title is looked for in first |
+| `document` | for `"document"`, which one, as a link names it: `Plan`, `[[Plan]]` or `/n/work/plan` |
+| `note` | the id of the scratch note the text came from; it is dropped once the text has landed |
- `"to": "card"` puts a card in the inbox of the space named by `space`, or else
of the first space that has a board.
- `"to": "diary"` adds `- 14:05 call the bank` to the end of today's entry, in
`WORKSPACE_TIMEZONE`, and starts the entry from `templates/day` when there is
none yet.
+- `"to": "note"` puts it on the scratchpad, for when it is not clear yet where
+ it belongs.
+- `"to": "document"` adds it as a paragraph at the end of a document. The text
+ it replaces is kept in the document's history, as a save keeps it.
The answer says where it went: `{"to":"card","id":7,"where":"work/backlog"}`.
diff --git a/docs/wiki/Commands.md b/docs/wiki/Commands.md
index a6c3217..9ffe5c4 100644
--- a/docs/wiki/Commands.md
+++ b/docs/wiki/Commands.md
@@ -50,7 +50,7 @@ arguments and `↑` / `↓` recall earlier lines.
| `[d / ]d` | the diary entry before / after the one open, by date |
| `Ctrl-P` | find anything, in every space — Enter opens the result lit beside them, n goes on |
| `b` | the board, in a tile: this space's, or the last one shown — 1 … 9 there shows another's |
-| `c` | capture a card into this space's inbox, without leaving here — reading a document, a card about it |
+| `c` | capture a card into this space's inbox, without leaving here — reading a document, a card about it; Tab on the line makes it a note, or a line in today's entry |
| `Esc · q` | close the document, and its tile when there are several |
| `/` | full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one |
| `n / N` | next / previous search result, opened beside the results — :noh clears the search |
@@ -127,6 +127,8 @@ The first list is the inbox, the last list is done.
| `o` | a new note, written straight away — it saves itself |
| `Enter · e · i` | write the note — Esc in normal mode, or :q, comes back to the notes |
| `K` | keep the note: a document in the folder being browsed, titled by its first line — :keep names it |
+| `C` | make the note a card where :card puts one — the first line its title, the rest its description |
+| `A` | put the note at the end of a document — :append, with the one on screen filled in; Tab completes a title |
| `dd` | drop the note (asks y/N) |
| `q · Esc · Q` | raise the scratchpad |
@@ -173,10 +175,11 @@ do: `:w!`, `:space! `, `:import!`.
| command | what it does |
| --- | --- |
| `:scratch, :sc [text]` | the scratchpad, lowered over whatever is on screen — Q does the same; with text, a note on it without leaving |
+| `:append [document]` | put the scratch note at the end of a document, by its title — Tab completes it — or the one on screen, and take it off the scratchpad |
| `:keep [title]` | make the scratch note a document in the folder being browsed — titled by its first line unless given one — and take it off the scratchpad |
| `:new, :n, :o [template] [yyyy-mm-dd] [name]` | start a new document here — from one of the space's templates, dated, named; in the diary from templates/day |
| `:cal, :calendar` | the diary by month — h/l a day, j/k a week, H/L a month, Enter opens the day or starts it |
-| `:today` | open today's diary entry, starting one if there is none yet |
+| `:today [text]` | open today's diary entry, starting one if there is none yet; with text, a timed line at its end without going there |
| `:next` | open the next node down the list |
| `:prev` | open the previous node |
| `:first` | open the first node in the list |
diff --git a/docs/wiki/Files-and-Search.md b/docs/wiki/Files-and-Search.md
index d797271..73b9f38 100644
--- a/docs/wiki/Files-and-Search.md
+++ b/docs/wiki/Files-and-Search.md
@@ -59,6 +59,8 @@ previous result beside the list, and `:noh` clears the search.
Cards are searched too, by their title, description and checklist steps. They
are listed after the documents, under `cards`, with the board and list each is
on. `Enter` on one turns it over on its board; `n` and `N` only light it up.
+Notes on the scratchpad come last, under `notes`, whatever space is searched;
+`Enter` lowers the scratchpad at the note.

diff --git a/docs/wiki/Guide.md b/docs/wiki/Guide.md
index b2baf17..86d1010 100644
--- a/docs/wiki/Guide.md
+++ b/docs/wiki/Guide.md
@@ -84,7 +84,10 @@ More in [Writing](Writing.md).
In a meeting someone asks you to renew a certificate. Press `c`, type
`Renew the gateway certificate`, and `Enter`. The card is in this space's
-inbox, and you have not left the document you were reading.
+inbox, and you have not left the document you were reading. Not a task? `Tab`
+on the same line makes it a note on the scratchpad, and `Tab` again a timed line
+in today's diary entry. A note waits there until `K`, `C` or `A` turns it into a
+document, a card, or more of a document.
Press `b` to see the **board**, in a tile beside the document. A digit there
shows the board of another space.
diff --git a/docs/wiki/Writing.md b/docs/wiki/Writing.md
index 13cde3a..70340ae 100644
--- a/docs/wiki/Writing.md
+++ b/docs/wiki/Writing.md
@@ -67,6 +67,8 @@ a body and the time it was last touched, newest on top.
| `o` | a new note, written straight away |
| `Enter`, `e`, `i` | write the note under the cursor |
| `K` | keep it: a document in the folder being browsed (`:keep [title]`) |
+| `C` | make it a card, where `:card` puts one: the first line its title, the rest its description |
+| `A` | put it at the end of a document (`:append [document]`) |
| `dd` | drop the note (asks y/N) |
| `Q`, `q`, `Esc` | raise the scratchpad |
@@ -81,6 +83,15 @@ takes it off the scratchpad. Its first line is the title — `# Plan` or plain
`Plan` alike — and the rest the body; `:keep ` names it instead and
keeps every line. The document opens, and the drawer goes up.
+`C` makes the note a card on the list under the board's cursor, or in this
+space's inbox. `A` opens `:append` with the document on screen filled in;
+change it to any document's title — `Tab` completes it — and `Enter` puts the
+note at its end, as a paragraph of its own. The document's text before is kept
+in its history. Either way the note leaves the scratchpad, as it does with `K`.
+
+`/` finds notes too, listed under `notes` after the documents and the cards;
+`Enter` on one lowers the scratchpad with it under the cursor.
+
The notes are in the backup, and an export of the whole workspace carries
them in `.workspace.json`, so `:import! /` puts them back.
diff --git a/migrations/0028_scratch_fts.sql b/migrations/0028_scratch_fts.sql
new file mode 100644
index 0000000..2d2a2a2
--- /dev/null
+++ b/migrations/0028_scratch_fts.sql
@@ -0,0 +1,18 @@
+-- The scratchpad is where things are written down before it is clear where
+-- they belong, which makes it the first place to look for something half
+-- remembered. Search finds a note the way it finds a document or a card.
+CREATE VIRTUAL TABLE scratch_fts USING fts5 (body, tokenize = 'unicode61');
+
+CREATE TRIGGER scratch_ai AFTER INSERT ON scratch_notes BEGIN
+ INSERT INTO scratch_fts (rowid, body) VALUES (new.id, new.body);
+END;
+CREATE TRIGGER scratch_ad AFTER DELETE ON scratch_notes BEGIN
+ DELETE FROM scratch_fts WHERE rowid = old.id;
+END;
+CREATE TRIGGER scratch_au AFTER UPDATE OF body ON scratch_notes BEGIN
+ DELETE FROM scratch_fts WHERE rowid = old.id;
+ INSERT INTO scratch_fts (rowid, body) VALUES (new.id, new.body);
+END;
+
+-- The notes written before the triggers existed.
+INSERT INTO scratch_fts (rowid, body) SELECT id, body FROM scratch_notes;
diff --git a/src/routes/capture.rs b/src/routes/capture.rs
index 1c58ebc..9a1b95a 100644
--- a/src/routes/capture.rs
+++ b/src/routes/capture.rs
@@ -7,14 +7,16 @@ use axum::{extract::State, Json};
use chrono::{TimeZone, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
-use sqlx::SqlitePool;
+use sqlx::{SqliteConnection, SqlitePool};
use crate::{
auth::Capturer,
error::{AppError, AppResult},
routes::{
boards::{self, CardInput},
- diary, links, media, nodes, revisions,
+ diary, links, media,
+ nodes::{self, DOCUMENT},
+ revisions, scratch, trash,
},
state::AppState,
};
@@ -27,6 +29,10 @@ pub enum To {
Card,
/// A line in today's diary entry: something that happened.
Diary,
+ /// A note on the scratchpad: not clear yet where it belongs.
+ Note,
+ /// A paragraph at the end of a document: more of what it is about.
+ Document,
}
#[derive(Deserialize)]
@@ -34,18 +40,28 @@ pub struct CaptureInput {
pub text: String,
#[serde(default)]
pub to: To,
- /// The slug of the space whose inbox a card goes to. Without it, the
- /// first space that has a board.
+ /// The slug of the space whose inbox a card goes to, or where a
+ /// document's title is looked for first. Without it, the first space that
+ /// has a board, and a title in any space.
#[serde(default)]
pub space: Option,
+ /// The document text goes to the end of, as a link to it is written:
+ /// `Plan`, `[[Plan]]` or `/n/work/plan`.
+ #[serde(default)]
+ pub document: Option,
+ /// The scratch note the text comes from, which goes once the text has
+ /// landed: the way a note leaves the scratchpad.
+ #[serde(default)]
+ pub note: Option,
}
#[derive(Serialize, Debug)]
pub struct Captured {
pub to: To,
- /// The card, or the diary entry.
+ /// The card, the note, or the document — the diary entry among them.
pub id: i64,
- /// Where it went, for the shortcut to say: `work/backlog`, `diary/untitled`.
+ /// Where it went, for the shortcut to say: `work/backlog`, `diary/untitled`,
+ /// `scratch`.
#[serde(rename = "where")]
pub place: String,
}
@@ -63,7 +79,21 @@ pub async fn capture(
let captured = match input.to {
To::Card => card(&state.db, text, input.space.as_deref()).await?,
To::Diary => diary(&state.db, state.config.mail.timezone, text, crate::now()).await?,
+ To::Note => note(&state.db, text).await?,
+ To::Document => {
+ let Some(target) = input.document.as_deref() else {
+ return Err(AppError::BadRequest(
+ "which document? `document` names it, as a link would".into(),
+ ));
+ };
+ document(&state.db, text, target, input.space.as_deref()).await?
+ }
};
+ // Made first, dropped after: a failure in between leaves two copies,
+ // never none.
+ if let Some(note) = input.note.filter(|_| captured.to != To::Note) {
+ scratch::consume(&state.db, note).await?;
+ }
if let Capturer::Token(token) = who {
tracing::info!(token, to = ?captured.to, "captured into {}", captured.place);
}
@@ -92,6 +122,15 @@ async fn card(db: &SqlitePool, text: &str, space: Option<&str>) -> AppResult AppResult {
+ let note = scratch::insert(db, text).await?;
+ Ok(Captured {
+ to: To::Note,
+ id: note.id,
+ place: "scratch".into(),
+ })
+}
+
/// `- 14:05 text`, at the end of today's entry: the one `T` opens. With none
/// yet, the line starts it the way `T` would, from `templates/day` if there
/// is one.
@@ -100,33 +139,77 @@ async fn diary(db: &SqlitePool, tz: Tz, text: &str, now: i64) -> AppResult,
+) -> AppResult {
+ let mut tx = crate::db::begin_write(db).await?;
+ let space = match space {
+ Some(slug) => links::find(&mut tx, &format!("/n/{slug}"), None).await?,
+ None => None,
+ };
+ let id = links::find(&mut tx, target, space)
+ .await?
+ .ok_or_else(|| AppError::BadRequest(format!("no document {target}")))?;
+ let (kind, body): (String, String) =
+ sqlx::query_as("SELECT kind, body FROM nodes WHERE id = ?1")
+ .bind(id)
+ .fetch_one(&mut *tx)
+ .await?;
+ if kind != DOCUMENT || trash::in_trash(&mut tx, id).await? {
+ return Err(AppError::BadRequest(format!("{target} is not a document")));
+ }
+ let body = match body.trim_end() {
+ "" => format!("{text}\n"),
+ before => format!("{before}\n\n{text}\n"),
+ };
+ write(&mut tx, id, &body, true, crate::now()).await?;
+ tx.commit().await?;
+ landed(db, To::Document, id).await
+}
+
+/// A document's text written, as a save writes it: the revision before it
+/// kept when it is worth keeping, its media and links read again.
+async fn write(
+ tx: &mut SqliteConnection,
+ id: i64,
+ body: &str,
+ keep: bool,
+ now: i64,
+) -> AppResult<()> {
+ if keep {
+ revisions::keep(tx, id, body, revisions::Reason::Edit).await?;
}
sqlx::query("UPDATE nodes SET body = ?1, updated_at = ?2 WHERE id = ?3")
- .bind(&body)
+ .bind(body)
.bind(now)
.bind(id)
.execute(&mut *tx)
.await?;
- media::link_to_node(&mut tx, id, &body).await?;
- links::index(&mut tx, id, &body).await?;
- tx.commit().await?;
+ media::link_to_node(tx, id, body).await?;
+ links::index(tx, id, body).await
+}
+/// Where it went into a document, said by its path.
+async fn landed(db: &SqlitePool, to: To, id: i64) -> AppResult {
let place = nodes::path_of(db, id)
.await?
.iter()
.map(|crumb| crumb.slug.as_str())
.collect::>()
.join("/");
- Ok(Captured {
- to: To::Diary,
- id,
- place,
- })
+ Ok(Captured { to, id, place })
}
/// `HH:MM` on the wall clock of the configured timezone.
@@ -333,6 +416,100 @@ pub(crate) mod tests {
assert_eq!(found, 1);
}
+ /// A note goes on the scratchpad, and search finds it there.
+ #[tokio::test]
+ async fn a_note_lands_on_the_scratchpad_and_is_found() {
+ let state = state().await;
+ let app = crate::routes::api_router(1024).with_state(state.clone());
+ let (status, answer) = call(
+ &app,
+ "/capture",
+ TOKEN,
+ r#"{"text":"ask about the lease","to":"note"}"#,
+ )
+ .await;
+ assert_eq!(status, StatusCode::OK, "{answer}");
+ assert!(answer.contains(r#""where":"scratch""#), "{answer}");
+
+ let found: Vec = sqlx::query_scalar(
+ "SELECT n.body FROM scratch_notes n JOIN scratch_fts ON scratch_fts.rowid = n.id
+ WHERE scratch_fts MATCH '\"lea\"*'",
+ )
+ .fetch_all(&state.db)
+ .await
+ .unwrap();
+ assert_eq!(found, ["ask about the lease"]);
+ }
+
+ /// A note leaves the scratchpad by landing somewhere else: at the end of
+ /// a document named as a link names it, or on a card.
+ #[tokio::test]
+ async fn a_note_leaves_for_a_document_or_a_card() {
+ let state = state().await;
+ let work: i64 =
+ sqlx::query_scalar("SELECT id FROM nodes WHERE slug = 'work' AND parent_id IS NULL")
+ .fetch_one(&state.db)
+ .await
+ .unwrap();
+ let input = serde_json::from_value(serde_json::json!({
+ "parent_id": work, "kind": "document", "name": "Plan", "body": "the plan\n",
+ }))
+ .unwrap();
+ let session = crate::auth::Session { id: "test".into() };
+ let plan = crate::routes::nodes::create(
+ session,
+ axum::extract::State(state.clone()),
+ axum::Json(input),
+ )
+ .await
+ .unwrap()
+ .0
+ .id;
+ let note = |body: &'static str| super::scratch::insert(&state.db, body);
+ let first = note("and a step more").await.unwrap().id;
+ let second = note("pay rent\nbefore the 3rd").await.unwrap().id;
+
+ let app = crate::routes::api_router(1024).with_state(state.clone());
+ let body = serde_json::json!({
+ "text": "and a step more", "to": "document", "document": "[[plan]]", "note": first,
+ });
+ let (status, answer) = call(&app, "/capture", TOKEN, &body.to_string()).await;
+ assert_eq!(status, StatusCode::OK, "{answer}");
+ assert!(answer.contains(r#""where":"work/plan""#), "{answer}");
+ let text: String = sqlx::query_scalar("SELECT body FROM nodes WHERE id = ?1")
+ .bind(plan)
+ .fetch_one(&state.db)
+ .await
+ .unwrap();
+ assert_eq!(text, "the plan\n\nand a step more\n");
+
+ let body = serde_json::json!({ "text": "pay rent\nbefore the 3rd", "note": second });
+ assert_eq!(
+ call(&app, "/capture", TOKEN, &body.to_string()).await.0,
+ StatusCode::OK
+ );
+ let left: i64 = sqlx::query_scalar("SELECT count(*) FROM scratch_notes")
+ .fetch_one(&state.db)
+ .await
+ .unwrap();
+ assert_eq!(left, 0);
+
+ // Nothing it could mean, and the note stays where it was.
+ let kept = note("still here").await.unwrap().id;
+ for target in ["Nowhere", "/n/work"] {
+ let body = serde_json::json!({
+ "text": "x", "to": "document", "document": target, "note": kept,
+ });
+ let (status, _) = call(&app, "/capture", TOKEN, &body.to_string()).await;
+ assert_eq!(status, StatusCode::BAD_REQUEST, "{target}");
+ }
+ let left: i64 = sqlx::query_scalar("SELECT count(*) FROM scratch_notes")
+ .fetch_one(&state.db)
+ .await
+ .unwrap();
+ assert_eq!(left, 1);
+ }
+
#[test]
fn a_line_goes_after_a_list_or_a_blank_line_after_prose() {
assert_eq!(appended("", "- 10:00 x"), "- 10:00 x\n");
diff --git a/src/routes/links.rs b/src/routes/links.rs
index 2652c52..e47e3f7 100644
--- a/src/routes/links.rs
+++ b/src/routes/links.rs
@@ -212,6 +212,27 @@ async fn resolve(
}
}
+/// What a link written by hand leads to: `/n/work/plan` by its path, and
+/// `[[Plan]]` or a bare `Plan` by its title, looked for in `space` first —
+/// the same thing the link would find written in a document there.
+pub async fn find(
+ db: &mut SqliteConnection,
+ written: &str,
+ space: Option,
+) -> AppResult