use crate::fuzzy::fuzzy_match;
use reedline::{
History, HistoryItem, HistoryItemId, HistorySessionId, Result, SearchFilter, SearchQuery,
SqliteBackedHistory,
};
pub struct FuzzyHistory {
inner: SqliteBackedHistory,
fuzzy_enabled: bool,
}
impl FuzzyHistory {
pub fn new(inner: SqliteBackedHistory) -> Self {
Self {
inner,
fuzzy_enabled: true,
}
}
#[allow(dead_code)]
pub fn set_fuzzy_enabled(&mut self, enabled: bool) {
self.fuzzy_enabled = enabled;
}
#[allow(dead_code)]
pub fn is_fuzzy_enabled(&self) -> bool {
self.fuzzy_enabled
}
fn fuzzy_search(&self, query: SearchQuery, pattern: &str) -> Result<Vec<HistoryItem>> {
let mut filter = SearchFilter::anything(query.filter.session);
filter.hostname = query.filter.hostname.clone();
filter.cwd_exact = query.filter.cwd_exact.clone();
filter.cwd_prefix = query.filter.cwd_prefix.clone();
filter.exit_successful = query.filter.exit_successful;
let modified_query = SearchQuery {
direction: query.direction,
start_time: query.start_time,
end_time: query.end_time,
start_id: query.start_id,
end_id: query.end_id,
limit: Some(1000), filter,
};
let candidates = self.inner.search(modified_query)?;
let mut scored: Vec<(HistoryItem, u32)> = candidates
.into_iter()
.filter_map(|item| fuzzy_match(pattern, &item.command_line).map(|m| (item, m.score)))
.collect();
scored.sort_by(|a, b| b.1.cmp(&a.1));
let results: Vec<HistoryItem> = if let Some(limit) = query.limit {
scored
.into_iter()
.take(limit as usize)
.map(|(item, _)| item)
.collect()
} else {
scored.into_iter().map(|(item, _)| item).collect()
};
Ok(results)
}
}
impl History for FuzzyHistory {
fn save(&mut self, mut h: HistoryItem) -> Result<HistoryItem> {
if h.start_timestamp.is_none() {
h.start_timestamp = Some(chrono::Utc::now());
}
if h.cwd.is_none() {
h.cwd = std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned());
}
if h.hostname.is_none() {
h.hostname = Some(gethostname::gethostname().to_string_lossy().into_owned());
}
self.inner.save(h)
}
fn load(&self, id: HistoryItemId) -> Result<HistoryItem> {
self.inner.load(id)
}
fn count(&self, query: SearchQuery) -> Result<i64> {
self.inner.count(query)
}
fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>> {
if self.fuzzy_enabled
&& let Some(ref cmd_search) = query.filter.command_line
{
if let reedline::CommandLineSearch::Substring(pattern) = cmd_search
&& !pattern.is_empty()
{
let pattern = pattern.clone();
return self.fuzzy_search(query, &pattern);
}
}
self.inner.search(query)
}
fn update(
&mut self,
id: HistoryItemId,
updater: &dyn Fn(HistoryItem) -> HistoryItem,
) -> Result<()> {
self.inner.update(id, updater)
}
fn clear(&mut self) -> Result<()> {
self.inner.clear()
}
fn delete(&mut self, h: HistoryItemId) -> Result<()> {
self.inner.delete(h)
}
fn sync(&mut self) -> std::io::Result<()> {
self.inner.sync()
}
fn session(&self) -> Option<HistorySessionId> {
self.inner.session()
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_fuzzy_history_module_compiles() {
}
}