Skip to content
Merged
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
7 changes: 7 additions & 0 deletions crates/jp_attachment_bear_note/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ Fetch a list of notes tagged with a specific tag:
jp attachment add "bear://search/?tag=project/my-project"
```

Exclude archived notes from a search.
Archived notes still match, but their content is left out of the attachment:

```sh
jp attachment add "bear://search/?tag=project/my-project&exclude_archived=true"
```

List all added URIs:

```sh
Expand Down
62 changes: 48 additions & 14 deletions crates/jp_attachment_bear_note/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,20 @@ impl BearNotes {
fn query_to_uri(&self, query: &Query) -> Result<Url, Box<dyn Error + Send + Sync>> {
let (host, path, query_pairs) = match query {
Query::Get(path) => ("get", path, vec![]),
Query::Search { query, tags } => (
"search",
Query::Search {
query,
tags.clone()
tags,
exclude_archived,
} => {
let mut pairs: Vec<(String, String)> = tags
.iter()
.map(|t| ("tag".to_owned(), t.to_owned()))
.collect::<Vec<_>>(),
),
.collect();
if *exclude_archived {
pairs.push(("exclude_archived".to_owned(), "true".to_owned()));
}
("search", query, pairs)
}
};

let query_pairs = query_pairs
Expand Down Expand Up @@ -64,7 +70,16 @@ enum Query {
Get(String),

/// Search for a note by its title or content, optionally filtering by tags.
Search { query: String, tags: Vec<String> },
Search {
query: String,
tags: Vec<String>,

/// Drop archived notes from the results instead of attaching their full
/// content.
/// Archived notes still match; they're just not returned.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
exclude_archived: bool,
},
}

/// A note from the Bear note-taking app, formatted for attachment XML output.
Expand Down Expand Up @@ -179,12 +194,21 @@ fn uri_to_query(uri: &Url) -> Result<Query, Box<dyn Error + Send + Sync>> {
.map(Query::Get)?,
Some("get") => Query::Get(path),
Some("search") => {
let tags = query_pairs
.into_iter()
.filter_map(|(k, v)| if k == "tag" { Some(v) } else { None })
.collect();
let mut tags = vec![];
let mut exclude_archived = false;
for (k, v) in query_pairs {
match k.as_str() {
"tag" => tags.push(v),
"exclude_archived" => exclude_archived = v != "false" && v != "0",
_ => {}
}
}

Query::Search { query: path, tags }
Query::Search {
query: path,
tags,
exclude_archived,
}
}
// Shorthand: `bear:NOTE_ID`. Truly opaque form (no `//`, no
// leading `/`) — the path holds the note id directly. Gated on
Expand All @@ -207,7 +231,11 @@ fn get_notes(query: &Query, db: &BearDb) -> Result<Vec<Note>, Box<dyn Error + Se
.map(Note::from)
.collect(),

Query::Search { query, tags } => {
Query::Search {
query,
tags,
exclude_archived,
} => {
let matches = db
.search(&SearchParams {
queries: vec![query.clone()],
Expand All @@ -216,8 +244,14 @@ fn get_notes(query: &Query, db: &BearDb) -> Result<Vec<Note>, Box<dyn Error + Se
})
.map_err(|e| e.to_string())?;

// For each matched note, fetch the full note
let ids: Vec<_> = matches.iter().map(|m| m.note_id.as_str()).collect();
// Archived notes match like any other. When excluded, drop them
// before fetching content so their (often large) bodies never
// reach the assistant.
let ids: Vec<_> = matches
.iter()
.filter(|m| !*exclude_archived || !m.is_archived)
.map(|m| m.note_id.as_str())
.collect();
db.get_notes(&ids)
.map_err(|e| e.to_string())?
.into_iter()
Expand Down
32 changes: 32 additions & 0 deletions crates/jp_attachment_bear_note/src/lib_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,39 @@ fn test_uri_to_query() {
Ok(Query::Search {
query: "tag #1".to_string(),
tags: vec![],
exclude_archived: false,
}),
),
(
"bear://search/tag%20%231?tag=tag%20%232",
Ok(Query::Search {
query: "tag #1".to_string(),
tags: vec!["tag #2".to_string()],
exclude_archived: false,
}),
),
(
"bear://search/tag%20%231?tag=tag%20%232&tag=tag%20%233",
Ok(Query::Search {
query: "tag #1".to_string(),
tags: vec!["tag #2".to_string(), "tag #3".to_string()],
exclude_archived: false,
}),
),
(
"bear://search/?tag=foo&exclude_archived=true",
Ok(Query::Search {
query: String::new(),
tags: vec!["foo".to_string()],
exclude_archived: true,
}),
),
(
"bear://search/?tag=foo&exclude_archived=false",
Ok(Query::Search {
query: String::new(),
tags: vec!["foo".to_string()],
exclude_archived: false,
}),
),
(
Expand All @@ -81,3 +100,16 @@ fn test_uri_to_query() {
assert_eq!(query, expected);
}
}

#[test]
fn test_exclude_archived_round_trips() {
let handler = BearNotes::default();
let query = Query::Search {
query: String::new(),
tags: vec!["rfd/D46/review".to_string()],
exclude_archived: true,
};

let uri = handler.query_to_uri(&query).unwrap();
assert_eq!(uri_to_query(&uri).unwrap(), query);
}
7 changes: 4 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -1926,8 +1926,9 @@ _resolve-conversation TITLE:

# Internal: look up a Bear note (or notes) by tag.
#
# Resolves `bear://search/?tag=TAG` against the local Bear database. Outputs
# one of:
# Resolves `bear://search/?tag=TAG` against the local Bear database. Archived
# notes are excluded: they're kept for reference, not for feeding into a
# session. Outputs one of:
#
# FOUND <bear-uri> - at least one note matched; caller should attach URI
# EDIT - no notes matched; caller should add `--edit`
Expand All @@ -1941,7 +1942,7 @@ _bear-note TAG:
#!/usr/bin/env sh
set -eu

uri="bear://search/?tag={{TAG}}"
uri="bear://search/?tag={{TAG}}&exclude_archived=true"
if jp attachment print "$uri" 2>/dev/null | grep -q .; then
echo "FOUND $uri"
exit 0
Expand Down
Loading