From 19bd00f62005d07fc22ef72558e1102a7bb13b03 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Sun, 9 May 2021 13:03:56 +0530 Subject: Support fulltext search of commands (#75) --- atuin-client/src/database.rs | 26 +++++++++++++++++++++----- atuin-client/src/settings.rs | 11 +++++++++++ src/command/mod.rs | 1 + src/command/search.rs | 26 ++++++++++++++++++-------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/atuin-client/src/database.rs b/atuin-client/src/database.rs index e56a8df0..160c6054 100644 --- a/atuin-client/src/database.rs +++ b/atuin-client/src/database.rs @@ -13,6 +13,7 @@ use sqlx::sqlite::{ use sqlx::Row; use super::history::History; +use super::settings::SearchMode; #[async_trait] pub trait Database { @@ -34,7 +35,12 @@ pub trait Database { async fn last(&self) -> Result; async fn before(&self, timestamp: chrono::DateTime, count: i64) -> Result>; - async fn search(&self, limit: Option, query: &str) -> Result>; + async fn search( + &self, + limit: Option, + search_mode: SearchMode, + query: &str, + ) -> Result>; async fn query_history(&self, query: &str) -> Result>; } @@ -185,7 +191,7 @@ impl Database for Sqlite { // inject the unique check if unique { "where timestamp = ( - select max(timestamp) from history + select max(timestamp) from history where h.command = history.command )" } else { @@ -268,16 +274,26 @@ impl Database for Sqlite { Ok(res.0) } - async fn search(&self, limit: Option, query: &str) -> Result> { + async fn search( + &self, + limit: Option, + search_mode: SearchMode, + query: &str, + ) -> Result> { let query = query.to_string().replace("*", "%"); // allow wildcard char let limit = limit.map_or("".to_owned(), |l| format!("limit {}", l)); + let query = match search_mode { + SearchMode::Prefix => query, + SearchMode::FullText => format!("%{}", query), + }; + let res = sqlx::query( format!( "select * from history h - where command like ?1 || '%' + where command like ?1 || '%' and timestamp = ( - select max(timestamp) from history + select max(timestamp) from history where h.command = history.command ) order by timestamp desc {}", diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index 4ea4be84..7ccbaf32 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -10,6 +10,15 @@ use parse_duration::parse; pub const HISTORY_PAGE_SIZE: i64 = 100; +#[derive(Clone, Debug, Deserialize, Copy)] +pub enum SearchMode { + #[serde(rename = "prefix")] + Prefix, + + #[serde(rename = "fulltext")] + FullText, +} + #[derive(Clone, Debug, Deserialize)] pub struct Settings { pub dialect: String, @@ -19,6 +28,7 @@ pub struct Settings { pub db_path: String, pub key_path: String, pub session_path: String, + pub search_mode: SearchMode, // This is automatically loaded when settings is created. Do not set in // config! Keep secrets and settings apart. @@ -100,6 +110,7 @@ impl Settings { s.set_default("auto_sync", true)?; s.set_default("sync_frequency", "1h")?; s.set_default("sync_address", "https://api.atuin.sh")?; + s.set_default("search_mode", "prefix")?; if config_file.exists() { s.merge(ConfigFile::with_name(config_file.to_str().unwrap()))?; diff --git a/src/command/mod.rs b/src/command/mod.rs index b16aae4d..8af64cb0 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -114,6 +114,7 @@ impl AtuinCmd { query, } => { search::run( + &client_settings, cwd, exit, interactive, diff --git a/src/command/search.rs b/src/command/search.rs index af2a1e43..f49f16e2 100644 --- a/src/command/search.rs +++ b/src/command/search.rs @@ -16,6 +16,7 @@ use unicode_width::UnicodeWidthStr; use atuin_client::database::Database; use atuin_client::history::History; +use atuin_client::settings::{SearchMode, Settings}; use crate::command::event::{Event, Events}; @@ -130,10 +131,14 @@ impl State { } } -async fn query_results(app: &mut State, db: &mut (impl Database + Send + Sync)) -> Result<()> { +async fn query_results( + app: &mut State, + search_mode: SearchMode, + db: &mut (impl Database + Send + Sync), +) -> Result<()> { let results = match app.input.as_str() { "" => db.list(Some(200), true).await?, - i => db.search(Some(200), i).await?, + i => db.search(Some(200), search_mode, i).await?, }; app.results = results; @@ -149,6 +154,7 @@ async fn query_results(app: &mut State, db: &mut (impl Database + Send + Sync)) async fn key_handler( input: Key, + search_mode: SearchMode, db: &mut (impl Database + Send + Sync), app: &mut State, ) -> Option { @@ -165,11 +171,11 @@ async fn key_handler( } Key::Char(c) => { app.input.push(c); - query_results(app, db).await.unwrap(); + query_results(app, search_mode, db).await.unwrap(); } Key::Backspace => { app.input.pop(); - query_results(app, db).await.unwrap(); + query_results(app, search_mode, db).await.unwrap(); } Key::Down => { let i = match app.results_state.selected() { @@ -277,6 +283,7 @@ fn draw(f: &mut Frame<'_, T>, history_count: i64, app: &mut State) { #[allow(clippy::clippy::cast_possible_truncation)] async fn select_history( query: &[String], + search_mode: SearchMode, db: &mut (impl Database + Send + Sync), ) -> Result { let stdout = stdout().into_raw_mode()?; @@ -294,13 +301,13 @@ async fn select_history( results_state: ListState::default(), }; - query_results(&mut app, db).await?; + query_results(&mut app, search_mode, db).await?; loop { let history_count = db.history_count().await?; // Handle input if let Event::Input(input) = events.next()? { - if let Some(output) = key_handler(input, db, &mut app).await { + if let Some(output) = key_handler(input, search_mode, db, &mut app).await { return Ok(output); } } @@ -313,6 +320,7 @@ async fn select_history( // it is going to have a lot of args #[allow(clippy::clippy::clippy::too_many_arguments)] pub async fn run( + settings: &Settings, cwd: Option, exit: Option, interactive: bool, @@ -339,10 +347,12 @@ pub async fn run( }; if interactive { - let item = select_history(query, db).await?; + let item = select_history(query, settings.search_mode, db).await?; eprintln!("{}", item); } else { - let results = db.search(None, query.join(" ").as_str()).await?; + let results = db + .search(None, settings.search_mode, query.join(" ").as_str()) + .await?; // TODO: This filtering would be better done in the SQL query, I just // need a nice way of building queries. -- cgit v1.2.3