Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.

Expand Down
15 changes: 14 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ pub(crate) struct App<'a> {
pub(super) prompt_manager: PromptManager,
pub(super) buffer_before_history_navigation: Option<String>,
pub(super) inline_history_suggestion: Option<(Option<HistoryEntry>, 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<String>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
16 changes: 16 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>,
/// Which history entries inline suggestions prefer (global, cwd).
#[arg(long = "inline-history-prefer", value_name = "MODE")]
inline_history_prefer: Option<settings::InlineHistoryPrefer>,
/// 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<usize>,
/// 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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
53 changes: 46 additions & 7 deletions src/history/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down
Loading