1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! The saved-search breadcrumb shared by the Query panel and the Ctrl+K note
//! browser: which saved search the current query came from, with a `• edited`
//! marker once the live query diverges (any text divergence counts, including
//! the order directive — the stored query is saved verbatim). Sticky
//! provenance — it survives edits, and is dropped only when the query becomes
//! blank (host-defined: an empty field, or the panel's default backlinks
//! query) or another saved search is expanded.
//!
//! Both hosts embed one of these and forward two events to it —
//! [`on_query_consumed`](SavedSearchBreadcrumb::on_query_consumed) after a list
//! keystroke, and [`set`](SavedSearchBreadcrumb::set) for a programmatic apply —
//! then read [`border_title`](SavedSearchBreadcrumb::border_title) at render.
//! The state machine lives here, not in the hosts, so the rule is defined once.
/// Provenance for a query loaded from a saved search.
struct Provenance {
name: String,
/// The stored query (trimmed), the form the edited check compares against.
stored: String,
}
#[derive(Default)]
pub struct SavedSearchBreadcrumb {
pinned: Option<Provenance>,
}
impl SavedSearchBreadcrumb {
/// Pin provenance for a freshly expanded saved search (or clear it when
/// `name` is `None`). Used by the programmatic apply path (the Saved
/// Searches modal). A blank `stored_query` is treated as "nothing to
/// pin" so the breadcrumb never shows over an empty query.
pub fn set(&mut self, name: Option<String>, stored_query: &str) {
self.pinned = match name {
Some(name) if !stored_query.trim().is_empty() => Some(Provenance {
name,
stored: stored_query.trim().to_string(),
}),
_ => None,
};
}
/// React to a list keystroke that was consumed. `accepted` is the name of
/// a saved search just expanded (if any); `query` is the resulting live
/// query; `query_is_blank` is the host's notion of "no active query" (an
/// empty field, or the Query panel's default backlinks query).
///
/// - accepted + non-blank query → pin (a fresh expansion)
/// - accepted but blank query → clear (expanded to nothing)
/// - no accept, blank query → clear (field emptied / reset to default)
/// - no accept, non-blank query → keep (sticky; the `• edited` marker is
/// derived in [`label`](Self::label))
pub fn on_query_consumed(
&mut self,
accepted: Option<String>,
query: &str,
query_is_blank: bool,
) {
match accepted {
Some(name) => self.set(if query_is_blank { None } else { Some(name) }, query),
None if query_is_blank => self.pinned = None,
None => {}
}
}
/// The pinned saved-search name (provenance only, no edited marker), or
/// `None` when no saved search is active. Used to pre-fill the save-search
/// dialog with the name the query came from.
pub fn name(&self) -> Option<&str> {
self.pinned.as_ref().map(|p| p.name.as_str())
}
/// The breadcrumb label for the searchbox border: the saved-search name,
/// plus ` • edited` once `query` diverges from the stored query. The
/// stored query is saved verbatim, so any text divergence counts —
/// including the order directive (a sort change IS an edit). `None` when
/// no saved search is active.
pub fn label(&self, query: &str) -> Option<String> {
let p = self.pinned.as_ref()?;
Some(if query.trim() != p.stored {
format!("{} • edited", p.name)
} else {
p.name.clone()
})
}
/// The searchbox border title: the chevroned breadcrumb (`‹ name ›`) when a
/// saved search is active, else the host's `fallback` (e.g. `" Query"`).
pub fn border_title(&self, query: &str, fallback: &str) -> String {
match self.label(query) {
Some(label) => format!(" ‹ {label} › "),
None => fallback.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pinned(stored: &str) -> SavedSearchBreadcrumb {
let mut b = SavedSearchBreadcrumb::default();
b.set(Some("todo".into()), stored);
b
}
#[test]
fn unedited_label_is_the_name() {
assert_eq!(pinned("#todo").label("#todo").as_deref(), Some("todo"));
}
#[test]
fn diverged_query_marks_edited() {
assert_eq!(
pinned("#todo").label("#todox").as_deref(),
Some("todo • edited")
);
}
#[test]
fn order_directive_change_is_edited() {
// The stored query is saved verbatim, so any divergence counts as an
// edit — including the order directive (see CONTEXT.md).
assert_eq!(
pinned("#todo").label("#todo or:title").as_deref(),
Some("todo • edited")
);
}
#[test]
fn name_returns_pinned_provenance_without_edited_marker() {
let b = pinned("#todo");
assert_eq!(b.name(), Some("todo"));
let empty = SavedSearchBreadcrumb::default();
assert_eq!(empty.name(), None);
}
#[test]
fn set_with_blank_query_does_not_pin() {
let mut b = SavedSearchBreadcrumb::default();
b.set(Some("todo".into()), " ");
assert_eq!(b.label(" "), None);
}
#[test]
fn accept_with_blank_expansion_clears_rather_than_pins() {
let mut b = SavedSearchBreadcrumb::default();
// A saved search whose stored query is empty must not pin a breadcrumb
// over the now-empty field.
b.on_query_consumed(Some("empty".into()), "", true);
assert_eq!(b.label(""), None);
}
#[test]
fn blank_query_clears_sticky_breadcrumb() {
let mut b = pinned("#todo");
b.on_query_consumed(None, "", true);
assert_eq!(b.label(""), None);
}
#[test]
fn non_blank_edit_keeps_sticky_breadcrumb() {
let mut b = pinned("#todo");
b.on_query_consumed(None, "#todox", false);
assert_eq!(b.label("#todox").as_deref(), Some("todo • edited"));
}
#[test]
fn border_title_chevrons_label_else_fallback() {
assert_eq!(
pinned("#todo").border_title("#todo", " Query"),
" ‹ todo › "
);
assert_eq!(
SavedSearchBreadcrumb::default().border_title("#todo", " Query"),
" Query"
);
}
}