Support fulltext search of commands (#75)
This commit is contained in:
parent
07c5461013
commit
19bd00f620
4 changed files with 51 additions and 13 deletions
|
@ -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<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>>;
|
||||
}
|
||||
|
@ -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<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 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 {}",
|
||||
|
|
|
@ -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()))?;
|
||||
|
|
|
@ -114,6 +114,7 @@ impl AtuinCmd {
|
|||
query,
|
||||
} => {
|
||||
search::run(
|
||||
&client_settings,
|
||||
cwd,
|
||||
exit,
|
||||
interactive,
|
||||
|
|
|
@ -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<String> {
|
||||
|
@ -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<T: Backend>(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<String> {
|
||||
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<String>,
|
||||
exit: Option<i64>,
|
||||
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.
|
||||
|
|
Loading…
Reference in a new issue