132 lines
4 KiB
Rust
132 lines
4 KiB
Rust
|
|
//! `/find` -- open an incremental search over the conversation scrollback.
|
||
|
|
//!
|
||
|
|
//! In simple mode a bare `/` goes to the prompt, so simple-mode users can't
|
||
|
|
//! reach the vim `/` scrollback search. `/find` focuses the scrollback pane
|
||
|
|
//! and opens the same search from either mode.
|
||
|
|
|
||
|
|
use crate::app::actions::Action;
|
||
|
|
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
|
||
|
|
|
||
|
|
/// Open scrollback search via `/find`.
|
||
|
|
pub struct FindCommand;
|
||
|
|
|
||
|
|
impl SlashCommand for FindCommand {
|
||
|
|
fn name(&self) -> &str {
|
||
|
|
"find"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn description(&self) -> &str {
|
||
|
|
"Search the conversation scrollback"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn session_scoped(&self) -> bool {
|
||
|
|
true
|
||
|
|
}
|
||
|
|
|
||
|
|
fn usage(&self) -> &str {
|
||
|
|
"/find [text]"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn takes_args(&self) -> bool {
|
||
|
|
true
|
||
|
|
}
|
||
|
|
|
||
|
|
fn arg_placeholder(&self) -> Option<&str> {
|
||
|
|
Some("[text]")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Minimal mode has no interactive scrollback pane to search — the
|
||
|
|
/// terminal's own search covers it (K7/§6.13). Gated off with a message.
|
||
|
|
fn available_in_minimal(&self) -> bool {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&self, _ctx: &mut CommandExecCtx, args: &str) -> CommandResult {
|
||
|
|
// whitespace-only args open a blank search.
|
||
|
|
let initial = args.trim();
|
||
|
|
let query = (!initial.is_empty()).then(|| initial.to_string());
|
||
|
|
CommandResult::Action(Action::OpenScrollbackSearch(query))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use crate::acp::model_state::ModelState;
|
||
|
|
|
||
|
|
static DEFAULT_BUNDLE_STATE: crate::app::bundle::BundleState =
|
||
|
|
crate::app::bundle::BundleState {
|
||
|
|
has_cache: false,
|
||
|
|
version: String::new(),
|
||
|
|
personas: Vec::new(),
|
||
|
|
roles: Vec::new(),
|
||
|
|
agents: Vec::new(),
|
||
|
|
skills: Vec::new(),
|
||
|
|
persona_details: Vec::new(),
|
||
|
|
role_details: Vec::new(),
|
||
|
|
};
|
||
|
|
|
||
|
|
fn make_ctx(models: &ModelState) -> CommandExecCtx<'_> {
|
||
|
|
CommandExecCtx {
|
||
|
|
models,
|
||
|
|
session_id: None,
|
||
|
|
bundle_state: &DEFAULT_BUNDLE_STATE,
|
||
|
|
screen_mode: crate::app::ScreenMode::Inline,
|
||
|
|
pager_state: crate::settings::PagerLocalSnapshot::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn find_returns_open_scrollback_search_action() {
|
||
|
|
let models = ModelState::default();
|
||
|
|
let mut ctx = make_ctx(&models);
|
||
|
|
let cmd = FindCommand;
|
||
|
|
assert!(matches!(
|
||
|
|
cmd.run(&mut ctx, ""),
|
||
|
|
CommandResult::Action(Action::OpenScrollbackSearch(None))
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn find_with_word_carries_it_as_initial_query() {
|
||
|
|
let models = ModelState::default();
|
||
|
|
let mut ctx = make_ctx(&models);
|
||
|
|
let cmd = FindCommand;
|
||
|
|
let CommandResult::Action(Action::OpenScrollbackSearch(query)) = cmd.run(&mut ctx, "foo")
|
||
|
|
else {
|
||
|
|
panic!("/find foo must open scrollback search");
|
||
|
|
};
|
||
|
|
assert_eq!(query.as_deref(), Some("foo"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn find_with_blank_args_carries_no_initial_query() {
|
||
|
|
let models = ModelState::default();
|
||
|
|
let mut ctx = make_ctx(&models);
|
||
|
|
let cmd = FindCommand;
|
||
|
|
// Both a bare `/find` and whitespace-only args open a blank search.
|
||
|
|
for args in ["", " "] {
|
||
|
|
assert!(matches!(
|
||
|
|
cmd.run(&mut ctx, args),
|
||
|
|
CommandResult::Action(Action::OpenScrollbackSearch(None))
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn find_advertises_optional_text_arg() {
|
||
|
|
// Pins the slash-arg contract so completion-accept appends a trailing
|
||
|
|
// space and the `[text]` placeholder shows while typing; bare `/find`
|
||
|
|
// stays valid (args not required).
|
||
|
|
let cmd = FindCommand;
|
||
|
|
assert!(cmd.takes_args());
|
||
|
|
assert!(!cmd.args_required());
|
||
|
|
assert_eq!(cmd.arg_placeholder(), Some("[text]"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn not_available_in_minimal() {
|
||
|
|
// Native terminal search replaces in-app scrollback search in minimal.
|
||
|
|
assert!(!FindCommand.available_in_minimal());
|
||
|
|
}
|
||
|
|
}
|