Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] History API rework #747

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
00df4bc
update documentation for `HistoryItemId`
ClementNerma Jan 31, 2024
b8d522b
allow to create `HistorySessionId` from the outside
ClementNerma Jan 31, 2024
a23202b
Improve history API
ClementNerma Dec 7, 2023
ae2c007
Fix: substracting with overflow
ClementNerma Dec 7, 2023
6e9e3ec
Update `FileBackedHistory` for new history system
ClementNerma Dec 7, 2023
b957895
Fix: ranges and truncation
ClementNerma Dec 7, 2023
bb4bd2d
Fix: ID generation
ClementNerma Dec 7, 2023
295f5b6
Fix: last problems with `FileBackedHistory`
ClementNerma Dec 7, 2023
b1ee575
Update `SqliteBackedHistory` for new history system
ClementNerma Dec 8, 2023
d966d5a
Update tests
ClementNerma Dec 8, 2023
78200a1
Simplify IDs handling in tests
ClementNerma Dec 8, 2023
73ceef4
Add migration for SQLite backend
ClementNerma Dec 8, 2023
a022079
Add support for legacy file text format
ClementNerma Dec 8, 2023
1ff51dc
Fix: support for legacy file text format
ClementNerma Dec 8, 2023
564f606
Add comments + escaping for LIKE patterns
ClementNerma Dec 8, 2023
6e1ad73
Fix: database's 'user_version' pragma checking
ClementNerma Dec 8, 2023
8a32b0c
Remove debug instructions
ClementNerma Dec 8, 2023
2e6a16c
Use .gen() method on SmallRng instead of casting
ClementNerma Dec 8, 2023
4e6a974
Improve search query handling + remove TODO
ClementNerma Dec 8, 2023
22f5e78
Fix: missing index drop + typo
ClementNerma Dec 8, 2023
1211d3d
Complete rebase
ClementNerma Jan 30, 2024
c27edb8
fix description line not truncating in columnar menu (#739)
maxomatic458 Feb 2, 2024
20453ac
Bump to version 0.29 (#742)
kubouch Feb 6, 2024
d111b53
Merge branch 'nushell:main' into main
ClementNerma Feb 6, 2024
858b3b5
spacing
ClementNerma Feb 6, 2024
accb428
Merge branch 'nushell:main' into main
ClementNerma Feb 9, 2024
0f94e4b
Fix missing reference
ClementNerma Feb 9, 2024
40950a2
Update tests
ClementNerma Feb 9, 2024
0fef407
Explicit truncation behaviour
ClementNerma Feb 9, 2024
5fff6fa
Make a field public
ClementNerma Feb 9, 2024
9421aa9
Merge branch 'nushell:main' into main
ClementNerma Feb 12, 2024
0911696
Merge branch 'nushell:main' into main
ClementNerma Feb 13, 2024
ca6d390
Fix: resolve merge conflicts
ClementNerma Mar 11, 2024
f5c4b6d
Merge remote-tracking branch 'upstream/main'
ClementNerma Mar 21, 2024
7393638
Update lockfile
ClementNerma Mar 21, 2024
794eb53
Merge remote-tracking branch 'upstream/main'
ClementNerma Apr 11, 2024
7165a65
Merge branch 'nushell:main' into main
ClementNerma May 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ strum_macros = "0.26"
thiserror = "1.0.31"
unicode-segmentation = "1.9.0"
unicode-width = "0.1.9"
rand = { version = "0.8.5", default-features = false, features = [
"small_rng",
"getrandom",
] }
indexmap = "2.2.1"

[dev-dependencies]
gethostname = "0.4.0"
Expand Down
23 changes: 15 additions & 8 deletions examples/cwd_aware_hinter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@
// pressing "a" hints to abc.
// Up/Down or Ctrl p/n, to select next/previous match

use std::io;
use std::{
io,
sync::atomic::{AtomicI64, Ordering},
};

use reedline::HistoryItemId;

static COUNTER: AtomicI64 = AtomicI64::new(0);

fn create_item(cwd: &str, cmd: &str, exit_status: i64) -> reedline::HistoryItem {
use std::time::Duration;

use reedline::HistoryItem;
HistoryItem {
id: None,
id: HistoryItemId(COUNTER.fetch_add(1, Ordering::SeqCst)),
start_timestamp: None,
command_line: cmd.to_string(),
session_id: None,
Expand All @@ -32,13 +39,13 @@ fn create_filled_example_history(home_dir: &str, orig_dir: &str) -> Box<dyn reed
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
let mut history = Box::new(reedline::SqliteBackedHistory::in_memory().unwrap());

history.save(create_item(orig_dir, "dummy", 0)).unwrap(); // add dummy item so ids start with 1
history.save(create_item(orig_dir, "ls /usr", 0)).unwrap();
history.save(create_item(orig_dir, "pwd", 0)).unwrap();
history.save(&create_item(orig_dir, "dummy", 0)).unwrap(); // add dummy item so ids start with 1
history.save(&create_item(orig_dir, "ls /usr", 0)).unwrap();
history.save(&create_item(orig_dir, "pwd", 0)).unwrap();

history.save(create_item(home_dir, "cat foo", 0)).unwrap();
history.save(create_item(home_dir, "ls bar", 0)).unwrap();
history.save(create_item(home_dir, "rm baz", 0)).unwrap();
history.save(&create_item(home_dir, "cat foo", 0)).unwrap();
history.save(&create_item(home_dir, "ls bar", 0)).unwrap();
history.save(&create_item(home_dir, "rm baz", 0)).unwrap();

history
}
Expand Down
10 changes: 7 additions & 3 deletions src/completion/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,18 @@ impl<'menu> HistoryCompleter<'menu> {

#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicI64, Ordering};

use rstest::rstest;

use super::*;
use crate::*;

static COUNTER: AtomicI64 = AtomicI64::new(0);

fn new_history_item(command_line: &str) -> HistoryItem {
HistoryItem {
id: None,
id: HistoryItemId(COUNTER.fetch_add(1, Ordering::SeqCst)),
start_timestamp: None,
command_line: command_line.to_string(),
session_id: None,
Expand All @@ -105,7 +109,7 @@ mod tests {
let expected_history_size = command_line_history.len();
let mut history = FileBackedHistory::new(command_line_history.len())?;
for command_line in command_line_history {
history.save(new_history_item(command_line))?;
history.save(&new_history_item(command_line))?;
}
let input = "git s";
let mut sut = HistoryCompleter::new(&history);
Expand Down Expand Up @@ -144,7 +148,7 @@ mod tests {
) -> Result<()> {
let mut history = FileBackedHistory::new(history_items.len())?;
for history_item in history_items {
history.save(new_history_item(history_item))?;
history.save(&new_history_item(history_item))?;
}
let mut sut = HistoryCompleter::new(&history);
let actual: Vec<String> = sut
Expand Down
35 changes: 22 additions & 13 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,32 +1823,41 @@ impl Reedline {
fn submit_buffer(&mut self, prompt: &dyn Prompt) -> io::Result<EventStatus> {
let buffer = self.editor.get_buffer().to_string();
self.hide_hints = true;

// Additional repaint to show the content without hints etc.
if let Some(transient_prompt) = self.transient_prompt.take() {
self.repaint(transient_prompt.as_ref())?;
self.transient_prompt = Some(transient_prompt);
} else {
self.repaint(prompt)?;
}
if !buffer.is_empty() {
let mut entry = HistoryItem::from_command_line(&buffer);
entry.session_id = self.get_history_session_id();

if self
if !buffer.is_empty() {
let filtered = self
.history_exclusion_prefix
.as_ref()
.map(|prefix| buffer.starts_with(prefix))
.unwrap_or(false)
{
entry.id = Some(Self::FILTERED_ITEM_ID);
self.history_last_run_id = entry.id;
self.history_excluded_item = Some(entry);
.map_or(false, |prefix| buffer.starts_with(prefix));

let entry_id = if filtered {
Self::FILTERED_ITEM_ID
} else {
self.history.generate_id()
};

let mut entry = HistoryItem::from_command_line(&buffer, entry_id);

entry.session_id = self.get_history_session_id();

if filtered {
self.history.replace(&entry).expect("todo: error handling");
} else {
entry = self.history.save(entry).expect("todo: error handling");
self.history_last_run_id = entry.id;
self.history_excluded_item = None;
self.history.save(&entry).expect("todo: error handling");
}

self.history_last_run_id = Some(entry_id);
self.history_excluded_item = if filtered { Some(entry) } else { None };
}

self.run_edit_commands(&[EditCommand::Clear]);
self.editor.reset_undo_stack();

Expand Down
99 changes: 61 additions & 38 deletions src/history/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct SearchFilter {
/// Query for the command line content
pub command_line: Option<CommandLineSearch>,
/// Considered implementation detail for now
pub(crate) not_command_line: Option<String>, // to skip the currently shown value in up-arrow navigation
pub not_command_line: Option<String>, // to skip the currently shown value in up-arrow navigation
/// Filter based on the executing systems hostname
pub hostname: Option<String>,
/// Exact filter for the working directory
Expand Down Expand Up @@ -186,21 +186,26 @@ impl SearchQuery {
/// Represents a history file or database
/// Data could be stored e.g. in a plain text file, in a `JSONL` file, in a `SQLite` database
pub trait History: Send {
/// save a history item to the database
/// if given id is None, a new id is created and set in the return value
/// if given id is Some, the existing entry is updated
fn save(&mut self, h: HistoryItem) -> Result<HistoryItem>;
/// generate a unique identifier for a new [`HistoryItem`]
fn generate_id(&mut self) -> HistoryItemId;

/// save a new history item to the database
fn save(&mut self, h: &HistoryItem) -> Result<()>;

/// replace an existing history item in the database
fn replace(&mut self, h: &HistoryItem) -> Result<()>;

/// load a history item by its id
fn load(&self, id: HistoryItemId) -> Result<HistoryItem>;

/// retrieves the next unused session id

/// count the results of a query
fn count(&self, query: SearchQuery) -> Result<i64>;
fn count(&self, query: SearchQuery) -> Result<u64>;

/// return the total number of history items
fn count_all(&self) -> Result<i64> {
fn count_all(&self) -> Result<u64> {
self.count(SearchQuery::everything(SearchDirection::Forward, None))
}

/// return the results of a query
fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>>;

Expand All @@ -210,28 +215,37 @@ pub trait History: Send {
id: HistoryItemId,
updater: &dyn Fn(HistoryItem) -> HistoryItem,
) -> Result<()>;

/// delete all history items
fn clear(&mut self) -> Result<()>;

/// remove an item from this history
fn delete(&mut self, h: HistoryItemId) -> Result<()>;

/// ensure that this history is written to disk
fn sync(&mut self) -> std::io::Result<()>;

/// get the history session id
fn session(&self) -> Option<HistorySessionId>;
}

#[cfg(test)]
mod test {
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
const IS_FILE_BASED: bool = false;
#[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
const IS_FILE_BASED: bool = true;
use std::time::Duration;

use crate::HistorySessionId;

fn create_item(session: i64, cwd: &str, cmd: &str, exit_status: i64) -> HistoryItem {
HistoryItem {
id: None,
fn create_item(
hist: &mut dyn History,
session: i64,
cwd: &str,
cmd: &str,
exit_status: i64,
) -> Result<()> {
let id = hist.count_all()? + 1;

let item = HistoryItem {
id: HistoryItemId(id as i64),
start_timestamp: None,
command_line: cmd.to_string(),
session_id: Some(HistorySessionId::new(session)),
Expand All @@ -240,31 +254,38 @@ mod test {
duration: Some(Duration::from_millis(1000)),
exit_status: Some(exit_status),
more_info: None,
}
};

hist.save(&item)?;

// // Ensure the item was correctly inserted
// assert_eq!(hist.count_all()?, id);

Ok(())
}
use std::time::Duration;

use super::*;
fn create_filled_example_history() -> Result<Box<dyn History>> {
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
let mut history = crate::SqliteBackedHistory::in_memory()?;

#[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
let mut history = crate::FileBackedHistory::default();
#[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
history.save(create_item(1, "/", "dummy", 0))?; // add dummy item so ids start with 1
history.save(create_item(1, "/home/me", "cd ~/Downloads", 0))?; // 1
history.save(create_item(1, "/home/me/Downloads", "unzp foo.zip", 1))?; // 2
history.save(create_item(1, "/home/me/Downloads", "unzip foo.zip", 0))?; // 3
history.save(create_item(1, "/home/me/Downloads", "cd foo", 0))?; // 4
history.save(create_item(1, "/home/me/Downloads/foo", "ls", 0))?; // 5
history.save(create_item(1, "/home/me/Downloads/foo", "ls -alh", 0))?; // 6
history.save(create_item(1, "/home/me/Downloads/foo", "cat x.txt", 0))?; // 7

history.save(create_item(1, "/home/me", "cd /etc/nginx", 0))?; // 8
history.save(create_item(1, "/etc/nginx", "ls -l", 0))?; // 9
history.save(create_item(1, "/etc/nginx", "vim nginx.conf", 0))?; // 10
history.save(create_item(1, "/etc/nginx", "vim htpasswd", 0))?; // 11
history.save(create_item(1, "/etc/nginx", "cat nginx.conf", 0))?; // 12

create_item(&mut history, 1, "/home/me", "cd ~/Downloads", 0)?; // 1
create_item(&mut history, 1, "/home/me/Downloads", "unzp foo.zip", 1)?; // 2
create_item(&mut history, 1, "/home/me/Downloads", "unzip foo.zip", 0)?; // 3
create_item(&mut history, 1, "/home/me/Downloads", "cd foo", 0)?; // 4
create_item(&mut history, 1, "/home/me/Downloads/foo", "ls", 0)?; // 5
create_item(&mut history, 1, "/home/me/Downloads/foo", "ls -alh", 0)?; // 6
create_item(&mut history, 1, "/home/me/Downloads/foo", "cat x.txt", 0)?; // 7

create_item(&mut history, 1, "/home/me", "cd /etc/nginx", 0)?; // 8
create_item(&mut history, 1, "/etc/nginx", "ls -l", 0)?; // 9
create_item(&mut history, 1, "/etc/nginx", "vim nginx.conf", 0)?; // 10
create_item(&mut history, 1, "/etc/nginx", "vim htpasswd", 0)?; // 11
create_item(&mut history, 1, "/etc/nginx", "cat nginx.conf", 0)?; // 12

Ok(Box::new(history))
}

Expand Down Expand Up @@ -298,7 +319,9 @@ mod test {
.iter()
.map(|id| history.load(HistoryItemId::new(*id)))
.collect::<Result<Vec<HistoryItem>>>()?;

assert_eq!(res, wanted);

Ok(())
}

Expand All @@ -310,7 +333,7 @@ mod test {
history.search(SearchQuery::everything(SearchDirection::Forward, None))
);

assert_eq!(history.count_all()?, if IS_FILE_BASED { 13 } else { 12 });
assert_eq!(history.count_all()?, 12);
Ok(())
}

Expand All @@ -330,7 +353,7 @@ mod test {
limit: Some(1),
..SearchQuery::everything(SearchDirection::Forward, None)
})?;
search_returned(&*history, res, vec![if IS_FILE_BASED { 0 } else { 1 }])?;
search_returned(&*history, res, vec![1])?;
Ok(())
}

Expand Down Expand Up @@ -427,8 +450,8 @@ mod test {

// create history, add a few entries
let mut history = open_history();
history.save(create_item(1, "/home/me", "cd ~/Downloads", 0))?; // 1
history.save(create_item(1, "/home/me/Downloads", "unzp foo.zip", 1))?; // 2
create_item(history.as_mut(), 1, "/home/me", "cd ~/Downloads", 0)?; // 1
create_item(history.as_mut(), 1, "/home/me/Downloads", "unzp foo.zip", 1)?; // 2
assert_eq!(history.count_all()?, 2);
drop(history);

Expand All @@ -450,7 +473,7 @@ mod test {
#[test]
fn history_size_zero() -> Result<()> {
let mut history = crate::FileBackedHistory::new(0)?;
history.save(create_item(1, "/home/me", "cd ~/Downloads", 0))?;
create_item(&mut history, 1, "/home/me", "cd ~/Downloads", 0)?;
assert_eq!(history.count_all()?, 0);
let _ = history.sync();
history.clear()?;
Expand Down
Loading
Loading