tui: apply command completions from keyboard
This commit is contained in:
parent
97f3df651a
commit
f6a3d2c6e5
|
|
@ -10,12 +10,21 @@ use crate::block::{
|
||||||
Block, CompactEvent, ThinkingBlock, ThinkingState, ToolCallBlock, ToolCallState,
|
Block, CompactEvent, ThinkingBlock, ThinkingState, ToolCallBlock, ToolCallState,
|
||||||
};
|
};
|
||||||
use crate::cache::FileCache;
|
use crate::cache::FileCache;
|
||||||
use crate::command::{CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry};
|
use crate::command::{
|
||||||
|
CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry,
|
||||||
|
};
|
||||||
use crate::input::InputBuffer;
|
use crate::input::InputBuffer;
|
||||||
use crate::scroll::Scroll;
|
use crate::scroll::Scroll;
|
||||||
use crate::task::TaskStore;
|
use crate::task::TaskStore;
|
||||||
use crate::ui::Mode;
|
use crate::ui::Mode;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CommandCompletionApply {
|
||||||
|
Applied,
|
||||||
|
Ambiguous,
|
||||||
|
NoCandidates,
|
||||||
|
}
|
||||||
|
|
||||||
/// In-flight completion popup state. Lives on `App` while the user is
|
/// In-flight completion popup state. Lives on `App` while the user is
|
||||||
/// typing inside a `@` / `#` / `/` token. Cleared whenever the trigger
|
/// typing inside a `@` / `#` / `/` token. Cleared whenever the trigger
|
||||||
/// is invalidated (cursor moved out, whitespace landed inside the
|
/// is invalidated (cursor moved out, whitespace landed inside the
|
||||||
|
|
@ -99,6 +108,7 @@ pub struct App {
|
||||||
pub command_input: InputBuffer,
|
pub command_input: InputBuffer,
|
||||||
pub input_mode: CommandInputMode,
|
pub input_mode: CommandInputMode,
|
||||||
pub command_registry: CommandRegistry,
|
pub command_registry: CommandRegistry,
|
||||||
|
command_completion_selected: Option<usize>,
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
|
||||||
/// records the instant; a second press within the timeout exits the
|
/// records the instant; a second press within the timeout exits the
|
||||||
|
|
@ -158,6 +168,7 @@ impl App {
|
||||||
command_input: InputBuffer::new(),
|
command_input: InputBuffer::new(),
|
||||||
input_mode: CommandInputMode::Composer,
|
input_mode: CommandInputMode::Composer,
|
||||||
command_registry: CommandRegistry::default(),
|
command_registry: CommandRegistry::default(),
|
||||||
|
command_completion_selected: None,
|
||||||
quit: false,
|
quit: false,
|
||||||
quit_confirm: None,
|
quit_confirm: None,
|
||||||
blocks: Vec::new(),
|
blocks: Vec::new(),
|
||||||
|
|
@ -1078,26 +1089,137 @@ impl App {
|
||||||
pub fn enter_command_mode(&mut self) {
|
pub fn enter_command_mode(&mut self) {
|
||||||
self.input_mode = CommandInputMode::Command;
|
self.input_mode = CommandInputMode::Command;
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
self.command_completion_selected = None;
|
||||||
self.quit_confirm = None;
|
self.quit_confirm = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exit_command_mode(&mut self) {
|
pub fn exit_command_mode(&mut self) {
|
||||||
self.input_mode = CommandInputMode::Composer;
|
self.input_mode = CommandInputMode::Composer;
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
|
self.command_completion_selected = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_command_input(&mut self) {
|
pub fn clear_command_input(&mut self) {
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
|
self.command_completion_selected = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command_text(&self) -> String {
|
pub fn command_text(&self) -> String {
|
||||||
self.command_input.plain_text()
|
self.command_input.plain_text()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn command_suggestions(&self) -> Vec<crate::command::CommandCandidate> {
|
pub fn command_suggestions(&self) -> Vec<CommandCandidate> {
|
||||||
self.command_registry.suggest(&self.command_text())
|
self.command_registry.suggest(&self.command_text())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn command_completion_selected(&self) -> Option<usize> {
|
||||||
|
let selected = self.command_completion_selected?;
|
||||||
|
(selected < self.command_suggestions().len()).then_some(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_completion_active(&self) -> bool {
|
||||||
|
!self.command_suggestions().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_command_completion_up(&mut self) {
|
||||||
|
let len = self.command_suggestions().len();
|
||||||
|
if len == 0 {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.command_completion_selected = Some(match self.command_completion_selected() {
|
||||||
|
Some(0) | None => len - 1,
|
||||||
|
Some(selected) => selected - 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_command_completion_down(&mut self) {
|
||||||
|
let len = self.command_suggestions().len();
|
||||||
|
if len == 0 {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.command_completion_selected = Some(match self.command_completion_selected() {
|
||||||
|
Some(selected) => (selected + 1) % len,
|
||||||
|
None => 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_command_completion(&mut self) -> CommandCompletionApply {
|
||||||
|
let suggestions = self.command_suggestions();
|
||||||
|
let candidate = match self.command_completion_selected() {
|
||||||
|
Some(selected) => suggestions.get(selected),
|
||||||
|
None if suggestions.len() == 1 => suggestions.first(),
|
||||||
|
None if suggestions.is_empty() => return CommandCompletionApply::NoCandidates,
|
||||||
|
None => return self.ambiguous_command_completion(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(candidate) = candidate else {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
return CommandCompletionApply::NoCandidates;
|
||||||
|
};
|
||||||
|
self.replace_command_name(candidate.name);
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
CommandCompletionApply::Applied
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn submit_command_with_completion(&mut self) -> Option<Method> {
|
||||||
|
let selected = self.command_completion_selected().is_some();
|
||||||
|
let command_text = self.command_text();
|
||||||
|
if command_text.trim().is_empty() && !selected {
|
||||||
|
return self.submit_command();
|
||||||
|
}
|
||||||
|
if !selected && self.command_name_is_complete(&command_text) {
|
||||||
|
return self.submit_command();
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.apply_command_completion() {
|
||||||
|
CommandCompletionApply::Applied | CommandCompletionApply::NoCandidates => {
|
||||||
|
self.submit_command()
|
||||||
|
}
|
||||||
|
CommandCompletionApply::Ambiguous => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ambiguous_command_completion(&mut self) -> CommandCompletionApply {
|
||||||
|
self.push_command_diagnostic(
|
||||||
|
"Ambiguous command completion; select a candidate with Up/Down or keep typing.",
|
||||||
|
);
|
||||||
|
CommandCompletionApply::Ambiguous
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_name_is_complete(&self, command_line: &str) -> bool {
|
||||||
|
let trimmed = command_line.trim_start();
|
||||||
|
let name = trimmed
|
||||||
|
.find(char::is_whitespace)
|
||||||
|
.map(|idx| &trimmed[..idx])
|
||||||
|
.unwrap_or(trimmed);
|
||||||
|
!name.is_empty() && self.command_registry.find(name).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_command_name(&mut self, canonical_name: &str) {
|
||||||
|
let command_line = self.command_text();
|
||||||
|
let leading_len = command_line.len() - command_line.trim_start().len();
|
||||||
|
let after_leading = &command_line[leading_len..];
|
||||||
|
let name_end = after_leading
|
||||||
|
.find(char::is_whitespace)
|
||||||
|
.map(|idx| leading_len + idx)
|
||||||
|
.unwrap_or(command_line.len());
|
||||||
|
let rest = &command_line[name_end..];
|
||||||
|
|
||||||
|
let mut completed = String::with_capacity(command_line.len().max(canonical_name.len() + 1));
|
||||||
|
completed.push_str(&command_line[..leading_len]);
|
||||||
|
completed.push_str(canonical_name);
|
||||||
|
if rest.is_empty() {
|
||||||
|
completed.push(' ');
|
||||||
|
} else {
|
||||||
|
completed.push_str(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_input.insert_str(&completed);
|
||||||
|
}
|
||||||
|
|
||||||
fn command_environment(&self) -> CommandEnvironment {
|
fn command_environment(&self) -> CommandEnvironment {
|
||||||
CommandEnvironment {
|
CommandEnvironment {
|
||||||
connected: self.connected,
|
connected: self.connected,
|
||||||
|
|
@ -1119,9 +1241,11 @@ impl App {
|
||||||
}
|
}
|
||||||
if result.clear_input {
|
if result.clear_input {
|
||||||
self.command_input.clear();
|
self.command_input.clear();
|
||||||
|
self.command_completion_selected = None;
|
||||||
}
|
}
|
||||||
if result.exit_command_mode {
|
if result.exit_command_mode {
|
||||||
self.input_mode = CommandInputMode::Composer;
|
self.input_mode = CommandInputMode::Composer;
|
||||||
|
self.command_completion_selected = None;
|
||||||
}
|
}
|
||||||
result.method
|
result.method
|
||||||
}
|
}
|
||||||
|
|
@ -1146,23 +1270,40 @@ impl App {
|
||||||
// stay readable. In command mode these operate on the command line,
|
// stay readable. In command mode these operate on the command line,
|
||||||
// keeping the normal composer buffer intact.
|
// keeping the normal composer buffer intact.
|
||||||
pub fn insert_char(&mut self, c: char) {
|
pub fn insert_char(&mut self, c: char) {
|
||||||
|
let command_mode = self.is_command_mode();
|
||||||
self.active_input_mut().insert_char(c);
|
self.active_input_mut().insert_char(c);
|
||||||
|
if command_mode {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn insert_newline(&mut self) {
|
pub fn insert_newline(&mut self) {
|
||||||
|
let command_mode = self.is_command_mode();
|
||||||
self.active_input_mut().insert_newline();
|
self.active_input_mut().insert_newline();
|
||||||
|
if command_mode {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn insert_paste(&mut self, content: String) {
|
pub fn insert_paste(&mut self, content: String) {
|
||||||
if self.is_command_mode() {
|
if self.is_command_mode() {
|
||||||
self.command_input.insert_str(&content);
|
self.command_input.insert_str(&content);
|
||||||
|
self.command_completion_selected = None;
|
||||||
} else {
|
} else {
|
||||||
self.input.insert_paste(content);
|
self.input.insert_paste(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn delete_char_before(&mut self) {
|
pub fn delete_char_before(&mut self) {
|
||||||
|
let command_mode = self.is_command_mode();
|
||||||
self.active_input_mut().delete_before();
|
self.active_input_mut().delete_before();
|
||||||
|
if command_mode {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn delete_char_after(&mut self) {
|
pub fn delete_char_after(&mut self) {
|
||||||
|
let command_mode = self.is_command_mode();
|
||||||
self.active_input_mut().delete_after();
|
self.active_input_mut().delete_after();
|
||||||
|
if command_mode {
|
||||||
|
self.command_completion_selected = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn move_cursor_left(&mut self) {
|
pub fn move_cursor_left(&mut self) {
|
||||||
self.active_input_mut().move_left();
|
self.active_input_mut().move_left();
|
||||||
|
|
|
||||||
|
|
@ -992,7 +992,7 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
app.exit_command_mode();
|
app.exit_command_mode();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Enter => app.submit_command(),
|
KeyCode::Enter => app.submit_command_with_completion(),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
if app.command_text().is_empty() {
|
if app.command_text().is_empty() {
|
||||||
app.exit_command_mode();
|
app.exit_command_mode();
|
||||||
|
|
@ -1014,11 +1014,19 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
|
if app.command_completion_active() {
|
||||||
|
app.move_command_completion_up();
|
||||||
|
} else {
|
||||||
app.move_cursor_up();
|
app.move_cursor_up();
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
|
if app.command_completion_active() {
|
||||||
|
app.move_command_completion_down();
|
||||||
|
} else {
|
||||||
app.move_cursor_down();
|
app.move_cursor_down();
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
|
|
@ -1029,7 +1037,10 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
||||||
app.move_cursor_end();
|
app.move_cursor_end();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
KeyCode::Tab => None,
|
KeyCode::Tab => {
|
||||||
|
app.apply_command_completion();
|
||||||
|
None
|
||||||
|
}
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
if key
|
if key
|
||||||
.modifiers
|
.modifiers
|
||||||
|
|
@ -1558,6 +1569,204 @@ mod tests {
|
||||||
assert_eq!(suggestions[0].name, "noop");
|
assert_eq!(suggestions[0].name, "noop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_tab_applies_unambiguous_candidate() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
enter_command_mode(&mut app);
|
||||||
|
type_keys(&mut app, "no");
|
||||||
|
|
||||||
|
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
|
||||||
|
|
||||||
|
assert!(app.is_command_mode());
|
||||||
|
assert_eq!(app.command_text(), "noop ");
|
||||||
|
assert_eq!(input_text(&app), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_enter_applies_and_executes_unambiguous_candidate() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
enter_command_mode(&mut app);
|
||||||
|
type_keys(&mut app, "no");
|
||||||
|
|
||||||
|
let method = handle_key(&mut app, key(KeyCode::Enter));
|
||||||
|
|
||||||
|
assert!(method.is_none());
|
||||||
|
assert!(!app.is_command_mode());
|
||||||
|
assert_eq!(input_text(&app), "");
|
||||||
|
assert!(has_alert(&app, "noop: no action"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_ambiguous_candidate_requires_selection_or_more_input() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
register_test_command(&mut app, "open", "open", parse_no_args, "open executed");
|
||||||
|
register_test_command(
|
||||||
|
&mut app,
|
||||||
|
"options",
|
||||||
|
"options",
|
||||||
|
parse_no_args,
|
||||||
|
"options executed",
|
||||||
|
);
|
||||||
|
enter_command_mode(&mut app);
|
||||||
|
type_keys(&mut app, "o");
|
||||||
|
|
||||||
|
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
|
||||||
|
assert_eq!(app.command_text(), "o");
|
||||||
|
assert!(app.is_command_mode());
|
||||||
|
assert!(has_alert(&app, "Ambiguous command completion"));
|
||||||
|
|
||||||
|
let before = app.blocks.len();
|
||||||
|
let method = handle_key(&mut app, key(KeyCode::Enter));
|
||||||
|
assert!(method.is_none());
|
||||||
|
assert_eq!(app.command_text(), "o");
|
||||||
|
assert!(app.is_command_mode());
|
||||||
|
assert!(app.blocks.len() > before);
|
||||||
|
assert!(!has_alert(&app, "open executed"));
|
||||||
|
assert!(!has_alert(&app, "options executed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_selected_candidate_applies_on_enter() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
register_test_command(&mut app, "open", "open", parse_no_args, "open executed");
|
||||||
|
register_test_command(
|
||||||
|
&mut app,
|
||||||
|
"options",
|
||||||
|
"options",
|
||||||
|
parse_no_args,
|
||||||
|
"options executed",
|
||||||
|
);
|
||||||
|
enter_command_mode(&mut app);
|
||||||
|
type_keys(&mut app, "o");
|
||||||
|
|
||||||
|
assert!(handle_key(&mut app, key(KeyCode::Down)).is_none());
|
||||||
|
let method = handle_key(&mut app, key(KeyCode::Enter));
|
||||||
|
|
||||||
|
assert!(method.is_none());
|
||||||
|
assert!(!app.is_command_mode());
|
||||||
|
assert!(has_alert(&app, "open executed"));
|
||||||
|
assert!(!has_alert(&app, "options executed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_argument_required_keeps_command_mode_after_name_completion() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
register_test_command(
|
||||||
|
&mut app,
|
||||||
|
"open",
|
||||||
|
"open <path>",
|
||||||
|
parse_required_arg,
|
||||||
|
"open executed",
|
||||||
|
);
|
||||||
|
enter_command_mode(&mut app);
|
||||||
|
type_keys(&mut app, "op");
|
||||||
|
|
||||||
|
let method = handle_key(&mut app, key(KeyCode::Enter));
|
||||||
|
|
||||||
|
assert!(method.is_none());
|
||||||
|
assert!(app.is_command_mode());
|
||||||
|
assert_eq!(app.command_text(), "open ");
|
||||||
|
assert!(has_alert(&app, "Invalid arguments. Usage: open <path>"));
|
||||||
|
assert!(!has_alert(&app, "open executed"));
|
||||||
|
assert_eq!(input_text(&app), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_completion_does_not_affect_normal_composer_without_popup() {
|
||||||
|
let mut app = App::new("agent".to_string());
|
||||||
|
type_keys(&mut app, "hello");
|
||||||
|
|
||||||
|
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
|
||||||
|
|
||||||
|
assert!(!app.is_command_mode());
|
||||||
|
assert_eq!(input_text(&app), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_command_mode(app: &mut App) {
|
||||||
|
assert!(handle_key(app, key(KeyCode::Char(':'))).is_none());
|
||||||
|
assert!(app.is_command_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_keys(app: &mut App, text: &str) {
|
||||||
|
for c in text.chars() {
|
||||||
|
assert!(handle_key(app, key(KeyCode::Char(c))).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(code: KeyCode) -> KeyEvent {
|
||||||
|
KeyEvent::new(code, KeyModifiers::NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_alert(app: &App, needle: &str) -> bool {
|
||||||
|
app.blocks.iter().any(|block| match block {
|
||||||
|
crate::block::Block::Alert { message, .. } => message.contains(needle),
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_test_command(
|
||||||
|
app: &mut App,
|
||||||
|
name: &'static str,
|
||||||
|
usage: &'static str,
|
||||||
|
argument_parser: crate::command::ArgumentParser,
|
||||||
|
message: &'static str,
|
||||||
|
) {
|
||||||
|
app.command_registry.register(crate::command::CommandSpec {
|
||||||
|
name,
|
||||||
|
aliases: &[],
|
||||||
|
usage,
|
||||||
|
description: "test command",
|
||||||
|
argument_parser,
|
||||||
|
can_execute: test_command_available,
|
||||||
|
executor: test_command_executor,
|
||||||
|
});
|
||||||
|
TEST_COMMAND_MESSAGES.with(|messages| messages.borrow_mut().push((name, message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static TEST_COMMAND_MESSAGES: std::cell::RefCell<Vec<(&'static str, &'static str)>> =
|
||||||
|
const { std::cell::RefCell::new(Vec::new()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_no_args(
|
||||||
|
raw: &str,
|
||||||
|
) -> Result<crate::command::CommandArgs, crate::command::CommandDiagnostic> {
|
||||||
|
Ok(crate::command::CommandArgs::parse_whitespace(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_required_arg(
|
||||||
|
raw: &str,
|
||||||
|
) -> Result<crate::command::CommandArgs, crate::command::CommandDiagnostic> {
|
||||||
|
let args = crate::command::CommandArgs::parse_whitespace(raw);
|
||||||
|
if args.argv().is_empty() {
|
||||||
|
return Err(crate::command::CommandDiagnostic::new(
|
||||||
|
"Invalid arguments. Usage: open <path>",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_command_available(
|
||||||
|
_environment: &crate::command::CommandEnvironment,
|
||||||
|
) -> Result<(), crate::command::CommandDiagnostic> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_command_executor(
|
||||||
|
invocation: crate::command::CommandInvocation<'_>,
|
||||||
|
) -> crate::command::CommandExecution {
|
||||||
|
let message = TEST_COMMAND_MESSAGES
|
||||||
|
.with(|messages| {
|
||||||
|
messages
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| *name == invocation.command.name)
|
||||||
|
.map(|(_, message)| *message)
|
||||||
|
})
|
||||||
|
.unwrap_or("test command executed");
|
||||||
|
crate::command::CommandExecution::notice(message)
|
||||||
|
}
|
||||||
|
|
||||||
fn input_text(app: &App) -> String {
|
fn input_text(app: &App) -> String {
|
||||||
protocol::Segment::flatten_to_text(&app.input.submit_segments())
|
protocol::Segment::flatten_to_text(&app.input.submit_segments())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -284,11 +284,29 @@ fn draw_command_popup(frame: &mut Frame, app: &App, input_area: Rect) {
|
||||||
.add_modifier(Modifier::BOLD);
|
.add_modifier(Modifier::BOLD);
|
||||||
let description_style = Style::default().fg(Color::DarkGray);
|
let description_style = Style::default().fg(Color::DarkGray);
|
||||||
let mut lines: Vec<Line<'static>> = Vec::with_capacity(popup_h as usize);
|
let mut lines: Vec<Line<'static>> = Vec::with_capacity(popup_h as usize);
|
||||||
for candidate in visible_suggestions.iter().take(popup_h as usize) {
|
let selected = app.command_completion_selected();
|
||||||
|
for (idx, candidate) in visible_suggestions
|
||||||
|
.iter()
|
||||||
|
.take(popup_h as usize)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let selected_style = if Some(idx) == selected {
|
||||||
|
Style::default()
|
||||||
|
.bg(Color::DarkGray)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(candidate.name.to_owned(), command_style),
|
Span::styled(
|
||||||
Span::styled(" — ", description_style),
|
candidate.name.to_owned(),
|
||||||
Span::styled(candidate.description.to_owned(), description_style),
|
command_style.patch(selected_style),
|
||||||
|
),
|
||||||
|
Span::styled(" — ", description_style.patch(selected_style)),
|
||||||
|
Span::styled(
|
||||||
|
candidate.description.to_owned(),
|
||||||
|
description_style.patch(selected_style),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user