diff --git a/README.md b/README.md index 91a9ec7b..ea090963 100644 --- a/README.md +++ b/README.md @@ -556,7 +556,10 @@ You can access a list of commands in the current Bash session that you Ctrl+C'd This is useful if you start writing a command, realise you want to run another command first, but you don't want to lose your first command. **Inline suggestions:** -Inline suggestions appear as you type based on the most recent matching history entry. Accept them by moving your cursor to the end of the line and pressing `Right`/`End`, or accept them one word at a time with `Ctrl+Right` (whitespace words) or `Alt+Right` (word parts, stopping at punctuation and path segments). +Inline suggestions show the most recent matching history entry as you type. +- Accept the whole suggestion with `Right`/`End` when the cursor is at the end of the line. +- Accept one word at a time with `Ctrl+Right` (whitespace words) or `Alt+Right` (word parts, stopping at punctuation and path segments). +- With the [JSONL history backend](#flyline-jsonl-history-backend), entries run in the current directory are preferred (`flyline editor --inline-history-prefer global|cwd`). [![Inline history demo](https://github.com/HalFrgrd/flyline/releases/download/assets/demo_inline_history.gif)](https://github.com/HalFrgrd/evp) @@ -575,6 +578,7 @@ Flyline can use a structured JSONL history storage backend (`~/.local/share/flyl The main improvements are: - **Real-time syncing between sessions**: Synchronize command history instantly across open terminal windows and tabs. - **Rich event metadata**: Store start/duration times, working directory (`cwd`), hostname, session UUID, command exit status (`$?` and `$PIPESTATUS`). +- **Directory-aware inline suggestions**: Prefer history entries run in the current directory. Flyline reads from the JSONL file when you open fuzzy history search. This means pressing `Up ` will show the last command from the current session until you open fuzzy history search. diff --git a/src/app/mod.rs b/src/app/mod.rs index dac0c36b..c2c724c9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -363,6 +363,8 @@ pub(crate) struct App<'a> { pub(super) prompt_manager: PromptManager, pub(super) buffer_before_history_navigation: Option, pub(super) inline_history_suggestion: Option<(Option, String)>, + /// Shell cwd at startup, used to prefer same-directory inline history suggestions. + pub(super) cwd: String, /// Buffer contents at the time the user last dismissed the inline suggestion. /// While the buffer equals this value the suggestion is suppressed. pub(super) dismissed_inline_suggestion_buffer: Option, @@ -525,6 +527,7 @@ impl<'a> App<'a> { ), buffer_before_history_navigation: None, inline_history_suggestion: None, + cwd: shell::backend().cwd(), dismissed_inline_suggestion_buffer: None, dismissed_tab_completion_wuc: None, dismissed_agent_mode_buffer: None, @@ -2480,11 +2483,21 @@ impl<'a> App<'a> { return None; } + let same_cwd = |e: &HistoryEntry| e.cwd() == Some(self.cwd.as_str()); + let prefer: Option<&dyn Fn(&HistoryEntry) -> bool> = + match crate::settings().inline_history_prefer { + settings::InlineHistoryPrefer::Global => None, + settings::InlineHistoryPrefer::Cwd => Some(&same_cwd), + }; if crate::settings().show_inline_history && let Some((entry, suffix)) = self .long_lived .history_manager - .get_command_suggestion_suffix(self.buffer.buffer()) + .get_command_suggestion_suffix( + self.buffer.buffer(), + prefer, + crate::settings().inline_history_prefer_window, + ) { return Some((Some(entry), suffix)); } diff --git a/src/cli.rs b/src/cli.rs index 2d4d3597..9dc20967 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -588,6 +588,12 @@ enum Commands { /// Show metadata (index and timestamp) for inline history suggestions. #[arg(long = "show-inline-history-metadata", default_missing_value = "true", num_args = 0..=1)] show_inline_history_metadata: Option, + /// Which history entries inline suggestions prefer (global, cwd). + #[arg(long = "inline-history-prefer", value_name = "MODE")] + inline_history_prefer: Option, + /// How many recent history entries to scan for a preferred inline suggestion (default: 1000). + #[arg(long = "inline-history-prefer-window", value_name = "NUM")] + inline_history_prefer_window: Option, /// Whether mouse clicks and drags on the command buffer change the /// cursor position and selection. Default is `true`. When `false`, /// mouse interaction with the buffer does not change the selection. @@ -1541,6 +1547,8 @@ pub(crate) fn call(words: *const bash_symbols::WordList) -> c_int { auto_close_chars, show_inline_history, show_inline_history_metadata, + inline_history_prefer, + inline_history_prefer_window, select_with_mouse, }) => { if let Some(enabled) = auto_close_chars { @@ -1555,6 +1563,14 @@ pub(crate) fn call(words: *const bash_symbols::WordList) -> c_int { log::info!("Inline history metadata set to {}", enabled); settings.show_inline_history_metadata = enabled; } + if let Some(prefer) = inline_history_prefer { + log::info!("Inline history prefer set to {:?}", prefer); + settings.inline_history_prefer = prefer; + } + if let Some(window) = inline_history_prefer_window { + log::info!("Inline history prefer window set to {}", window); + settings.inline_history_prefer_window = window; + } if let Some(enabled) = select_with_mouse { log::info!("Select with mouse set to {}", enabled); settings.select_with_mouse = enabled; diff --git a/src/history/mod.rs b/src/history/mod.rs index 1d4e109d..7e9c47f0 100644 --- a/src/history/mod.rs +++ b/src/history/mod.rs @@ -921,16 +921,23 @@ impl HistoryManager { res } + /// Most recent history entry starting with `command`, preferring one that + /// satisfies `prefer` if it is within the last `window` entries. pub fn get_command_suggestion_suffix( - &mut self, + &self, command: &str, + prefer: Option<&dyn Fn(&HistoryEntry) -> bool>, + window: usize, ) -> Option<(HistoryEntry, String)> { - for entry in self.entries.iter().take(self.index).rev() { - if entry.command.starts_with(command) { - return Some((entry.clone(), entry.command[command.len()..].to_string())); - } - } - None + let recent = || self.entries[..self.index].iter().rev(); + let entry = prefer + .and_then(|prefer| { + recent() + .take(window) + .find(|e| e.command.starts_with(command) && prefer(e)) + }) + .or_else(|| recent().find(|e| e.command.starts_with(command)))?; + Some((entry.clone(), entry.command[command.len()..].to_string())) } pub fn search_in_history( @@ -1723,6 +1730,38 @@ git status ); } + #[test] + fn test_command_suggestion_prefers_within_window() { + let entry = |cmd: &str, cwd: &str| { + let mut e = HistoryEntry::new(None, 0, cmd.to_string()); + e.fill_missing_metadata(None, Some(cwd.to_string()), None, None); + e + }; + let mut hm = HistoryManager::new_empty_with_path(None); + hm.entries = vec![ + entry("make test", "/a"), + entry("make build", "/b"), + entry("ls", "/b"), + ]; + hm.index = hm.entries.len(); + + let suggest = |cwd: &str, window| { + let prefer = |e: &HistoryEntry| e.cwd() == Some(cwd); + hm.get_command_suggestion_suffix("make", Some(&prefer), window) + .map(|(_, suffix)| suffix) + }; + assert_eq!(suggest("/a", 3).as_deref(), Some(" test")); + assert_eq!(suggest("/c", 3).as_deref(), Some(" build")); + // A preferred match outside the window falls back to global. + assert_eq!(suggest("/a", 2).as_deref(), Some(" build")); + assert_eq!( + hm.get_command_suggestion_suffix("make", None, 3) + .map(|(_, suffix)| suffix) + .as_deref(), + Some(" build") + ); + } + #[test] fn test_last_word_insert_logic() { let mut hm = HistoryManager::default(); diff --git a/src/settings.rs b/src/settings.rs index 3d2e9226..95423387 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -39,6 +39,19 @@ pub enum SuggestionSortOrder { Alphabetical, } +/// Which history entries inline suggestions should prefer. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, serde::Serialize, serde::Deserialize, +)] +pub enum InlineHistoryPrefer { + /// Most recent match, regardless of where it was run. + Global, + /// Most recent match run in the current directory, falling back to global. + /// Requires the flyline history backend, since bash history has no cwd. + #[default] + Cwd, +} + /// Controls fuzzy matching behavior for suggestions. #[derive( Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum, serde::Serialize, serde::Deserialize, @@ -296,6 +309,10 @@ pub struct Settings { pub show_inline_history: bool, /// Whether to show metadata (index and timestamp) for inline history suggestions. pub show_inline_history_metadata: bool, + /// Which history entries inline suggestions should prefer. + pub inline_history_prefer: InlineHistoryPrefer, + /// How many recent history entries to scan for a preferred inline suggestion. + pub inline_history_prefer_window: usize, /// Whether to auto-start tab completion suggestions as you type. pub auto_suggest: bool, /// Whether to show inline suggestions from auto tab completions. @@ -411,6 +428,8 @@ impl Default for Settings { fuzzy_mode: FuzzyMode::default(), num_suggestion_rows: 12, show_inline_history_metadata: true, + inline_history_prefer: InlineHistoryPrefer::default(), + inline_history_prefer_window: 1000, auto_close_chars: true, select_with_mouse: true, cursor_config: CursorConfig::default(),