Skip to main content

datui_lib/widgets/text_input/
history.rs

1//! Recall of submitted values for [`super::TextInput`]. Each history's id names its
2//! cache file (query, SQL and fuzzy keep separate lists); loaded lazily on first use.
3//! Reads and writes go through [`CacheManager`] under a lock across read-modify-write,
4//! so concurrent instances merge.
5
6use color_eyre::Result;
7
8use crate::cache::CacheManager;
9
10/// Append an entry unless it repeats the previous one (only consecutive duplicates are
11/// dropped).
12pub fn push_entry(entries: &mut Vec<String>, entry: String) {
13    if entries.last() == Some(&entry) {
14        return;
15    }
16    entries.push(entry);
17}
18
19/// Stands for a line break on disk. The file holds one entry per line, and a
20/// statement can span several; U+2028 is Unicode's own line separator, and
21/// nothing typed into a query plausibly holds one.
22const LINE_BREAK_ON_DISK: char = '\u{2028}';
23
24fn to_disk(entry: &str) -> String {
25    entry.replace('\n', &LINE_BREAK_ON_DISK.to_string())
26}
27
28fn from_disk(entry: String) -> String {
29    if entry.contains(LINE_BREAK_ON_DISK) {
30        entry.replace(LINE_BREAK_ON_DISK, "\n")
31    } else {
32        entry
33    }
34}
35
36/// Drop the oldest entries until at most `limit` remain.
37fn trim(entries: &mut Vec<String>, limit: usize) {
38    let excess = entries.len().saturating_sub(limit);
39    entries.drain(..excess);
40}
41
42/// One input's recall state: the entries, where the user is in them, and the
43/// value that was being edited before they started walking back.
44#[derive(Debug, Clone, Default)]
45pub(super) struct InputHistory {
46    pub id: Option<String>,
47    entries: Vec<String>,
48    /// Index into `entries` while walking; `None` means editing a fresh value.
49    index: Option<usize>,
50    /// The in-progress value stashed when the walk began.
51    stash: Option<String>,
52    pub limit: usize,
53    loaded: bool,
54}
55
56impl InputHistory {
57    pub fn new(limit: usize) -> Self {
58        Self {
59            limit,
60            ..Self::default()
61        }
62    }
63
64    pub fn is_enabled(&self) -> bool {
65        self.id.is_some()
66    }
67
68    #[cfg(test)]
69    pub fn entries(&self) -> &[String] {
70        &self.entries
71    }
72
73    /// Load entries from the cache the first time they are needed.
74    pub fn ensure_loaded(&mut self, cache: &CacheManager) -> Result<()> {
75        if self.loaded {
76            return Ok(());
77        }
78        if let Some(id) = &self.id {
79            self.entries = cache
80                .load_history_file(id)?
81                .into_iter()
82                .map(from_disk)
83                .collect();
84            self.loaded = true;
85        }
86        Ok(())
87    }
88
89    /// Record `value` as newest and persist, merged with what is on disk so other instances'
90    /// entries survive.
91    pub fn remember(&mut self, value: &str, cache: &CacheManager) -> Result<()> {
92        let Some(id) = self.id.clone() else {
93            return Ok(());
94        };
95        if value.is_empty() {
96            return Ok(());
97        }
98        push_entry(&mut self.entries, value.to_string());
99        trim(&mut self.entries, self.limit);
100
101        let entry = to_disk(value);
102        let limit = self.limit;
103        cache.update_history_file(&id, move |entries| {
104            push_entry(entries, entry);
105            trim(entries, limit);
106        })?;
107        Ok(())
108    }
109
110    /// Add an entry without touching the cache. Test support.
111    #[cfg(test)]
112    pub fn seed(&mut self, entry: String) {
113        self.loaded = true;
114        push_entry(&mut self.entries, entry);
115    }
116
117    /// Stop walking the history without changing the current value.
118    pub fn reset_position(&mut self) {
119        self.index = None;
120        self.stash = None;
121    }
122
123    /// Step to an older entry, returning it; `current` is stashed on the first step so
124    /// walking back returns to it.
125    pub fn older(&mut self, current: &str, cache: Option<&CacheManager>) -> Option<String> {
126        self.id.as_ref()?;
127        if !self.loaded {
128            let cache = cache?;
129            self.ensure_loaded(cache)
130                .inspect_err(|e| log::warn!(target: "datui", "read input history: {e:#}"))
131                .ok()?;
132        }
133        if self.entries.is_empty() {
134            return None;
135        }
136        if self.index.is_none() {
137            self.stash = Some(current.to_string());
138        }
139        let index = match self.index {
140            Some(current) => current.saturating_sub(1),
141            None => self.entries.len() - 1,
142        };
143        self.index = Some(index);
144        self.entries.get(index).cloned()
145    }
146
147    /// Step to a newer entry, returning the value to show. Stepping past the
148    /// newest entry restores the stashed in-progress value.
149    pub fn newer(&mut self) -> Option<String> {
150        self.id.as_ref()?;
151        let index = self.index?;
152        if index + 1 >= self.entries.len() {
153            let stashed = self.stash.take();
154            self.index = None;
155            return stashed;
156        }
157        self.index = Some(index + 1);
158        self.entries.get(index + 1).cloned()
159    }
160}
161
162#[cfg(test)]
163mod tests;