Support fulltext search of commands (#75)

This commit is contained in:
Yuvi Panda 2021-05-09 13:03:56 +05:30 committed by GitHub
parent 07c5461013
commit 19bd00f620
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 51 additions and 13 deletions

View file

@ -13,6 +13,7 @@ use sqlx::sqlite::{
use sqlx::Row; use sqlx::Row;
use super::history::History; use super::history::History;
use super::settings::SearchMode;
#[async_trait] #[async_trait]
pub trait Database { pub trait Database {
@ -34,7 +35,12 @@ pub trait Database {
async fn last(&self) -> Result<History>; async fn last(&self) -> Result<History>;
async fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>>; async fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>>;
async fn search(&self, limit: Option<i64>, query: &str) -> Result<Vec<History>>; async fn search(
&self,
limit: Option<i64>,
search_mode: SearchMode,
query: &str,
) -> Result<Vec<History>>;
async fn query_history(&self, query: &str) -> Result<Vec<History>>; async fn query_history(&self, query: &str) -> Result<Vec<History>>;
} }
@ -268,10 +274,20 @@ impl Database for Sqlite {
Ok(res.0) Ok(res.0)
} }
async fn search(&self, limit: Option<i64>, query: &str) -> Result<Vec<History>> { async fn search(
&self,
limit: Option<i64>,
search_mode: SearchMode,
query: &str,
) -> Result<Vec<History>> {
let query = query.to_string().replace("*", "%"); // allow wildcard char let query = query.to_string().replace("*", "%"); // allow wildcard char
let limit = limit.map_or("".to_owned(), |l| format!("limit {}", l)); 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( let res = sqlx::query(
format!( format!(
"select * from history h "select * from history h

View file

@ -10,6 +10,15 @@ use parse_duration::parse;
pub const HISTORY_PAGE_SIZE: i64 = 100; 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)] #[derive(Clone, Debug, Deserialize)]
pub struct Settings { pub struct Settings {
pub dialect: String, pub dialect: String,
@ -19,6 +28,7 @@ pub struct Settings {
pub db_path: String, pub db_path: String,
pub key_path: String, pub key_path: String,
pub session_path: String, pub session_path: String,
pub search_mode: SearchMode,
// This is automatically loaded when settings is created. Do not set in // This is automatically loaded when settings is created. Do not set in
// config! Keep secrets and settings apart. // config! Keep secrets and settings apart.
@ -100,6 +110,7 @@ impl Settings {
s.set_default("auto_sync", true)?; s.set_default("auto_sync", true)?;
s.set_default("sync_frequency", "1h")?; s.set_default("sync_frequency", "1h")?;
s.set_default("sync_address", "https://api.atuin.sh")?; s.set_default("sync_address", "https://api.atuin.sh")?;
s.set_default("search_mode", "prefix")?;
if config_file.exists() { if config_file.exists() {
s.merge(ConfigFile::with_name(config_file.to_str().unwrap()))?; s.merge(ConfigFile::with_name(config_file.to_str().unwrap()))?;

View file

@ -114,6 +114,7 @@ impl AtuinCmd {
query, query,
} => { } => {
search::run( search::run(
&client_settings,
cwd, cwd,
exit, exit,
interactive, interactive,

View file

@ -16,6 +16,7 @@ use unicode_width::UnicodeWidthStr;
use atuin_client::database::Database; use atuin_client::database::Database;
use atuin_client::history::History; use atuin_client::history::History;
use atuin_client::settings::{SearchMode, Settings};
use crate::command::event::{Event, Events}; 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() { let results = match app.input.as_str() {
"" => db.list(Some(200), true).await?, "" => 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; app.results = results;
@ -149,6 +154,7 @@ async fn query_results(app: &mut State, db: &mut (impl Database + Send + Sync))
async fn key_handler( async fn key_handler(
input: Key, input: Key,
search_mode: SearchMode,
db: &mut (impl Database + Send + Sync), db: &mut (impl Database + Send + Sync),
app: &mut State, app: &mut State,
) -> Option<String> { ) -> Option<String> {
@ -165,11 +171,11 @@ async fn key_handler(
} }
Key::Char(c) => { Key::Char(c) => {
app.input.push(c); app.input.push(c);
query_results(app, db).await.unwrap(); query_results(app, search_mode, db).await.unwrap();
} }
Key::Backspace => { Key::Backspace => {
app.input.pop(); app.input.pop();
query_results(app, db).await.unwrap(); query_results(app, search_mode, db).await.unwrap();
} }
Key::Down => { Key::Down => {
let i = match app.results_state.selected() { let i = match app.results_state.selected() {
@ -277,6 +283,7 @@ fn draw<T: Backend>(f: &mut Frame<'_, T>, history_count: i64, app: &mut State) {
#[allow(clippy::clippy::cast_possible_truncation)] #[allow(clippy::clippy::cast_possible_truncation)]
async fn select_history( async fn select_history(
query: &[String], query: &[String],
search_mode: SearchMode,
db: &mut (impl Database + Send + Sync), db: &mut (impl Database + Send + Sync),
) -> Result<String> { ) -> Result<String> {
let stdout = stdout().into_raw_mode()?; let stdout = stdout().into_raw_mode()?;
@ -294,13 +301,13 @@ async fn select_history(
results_state: ListState::default(), results_state: ListState::default(),
}; };
query_results(&mut app, db).await?; query_results(&mut app, search_mode, db).await?;
loop { loop {
let history_count = db.history_count().await?; let history_count = db.history_count().await?;
// Handle input // Handle input
if let Event::Input(input) = events.next()? { 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); return Ok(output);
} }
} }
@ -313,6 +320,7 @@ async fn select_history(
// it is going to have a lot of args // it is going to have a lot of args
#[allow(clippy::clippy::clippy::too_many_arguments)] #[allow(clippy::clippy::clippy::too_many_arguments)]
pub async fn run( pub async fn run(
settings: &Settings,
cwd: Option<String>, cwd: Option<String>,
exit: Option<i64>, exit: Option<i64>,
interactive: bool, interactive: bool,
@ -339,10 +347,12 @@ pub async fn run(
}; };
if interactive { if interactive {
let item = select_history(query, db).await?; let item = select_history(query, settings.search_mode, db).await?;
eprintln!("{}", item); eprintln!("{}", item);
} else { } 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 // TODO: This filtering would be better done in the SQL query, I just
// need a nice way of building queries. // need a nice way of building queries.