feat: commandline edit --accept to instantly execute command (#16193)

# Description

Fixes 
- #11065

Revised 
- #15092

Depends on

- https://github.com/nushell/reedline/pull/933

Adds the `--accept` flag to the `commandline` command, to immediately
accept the input.

Example use case: atuin

# User-facing changes

Users get the ability to pass `--accept` or `-A` to `commandline edit`
in order to immediately execute the resulting commandline.

# Tests + Formatting

I added two test cases that execute `commandline edit -A`.
There is also some documentation about their unintuitive expectations
output.

# After Submitting

The [docs](https://www.nushell.sh/commands/docs/commandline_edit.html)
can be updated to the new flag:

--accept, -A: immediately execute the command (no additional return
required)

> [!NOTE]
>
> This PR will be revised if / when
https://github.com/nushell/reedline/pull/933 is accepted

# Demo


[![asciicast](https://asciinema.org/a/Ql4r1oWu8J0C4MecmIZjxw5Pg.svg)](https://asciinema.org/a/Ql4r1oWu8J0C4MecmIZjxw5Pg)
This commit is contained in:
Stuart Carnie
2025-08-18 06:24:17 +10:00
committed by GitHub
parent a40f6d5cba
commit fd4a04211a
4 changed files with 33 additions and 3 deletions

View File

@@ -26,6 +26,11 @@ impl Command for CommandlineEdit {
"replaces the current contents of the buffer (default)", "replaces the current contents of the buffer (default)",
Some('r'), Some('r'),
) )
.switch(
"accept",
"immediately executes the result after edit",
Some('A'),
)
.required( .required(
"str", "str",
SyntaxShape::String, SyntaxShape::String,
@@ -61,6 +66,9 @@ impl Command for CommandlineEdit {
repl.buffer = str; repl.buffer = str;
repl.cursor_pos = repl.buffer.len(); repl.cursor_pos = repl.buffer.len();
} }
repl.accept = call.has_flag(engine_state, stack, "accept")?;
Ok(Value::nothing(call.head).into_pipeline_data()) Ok(Value::nothing(call.head).into_pipeline_data())
} }
} }

View File

@@ -491,7 +491,9 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
// CLEAR STACK-REFERENCE 1 // CLEAR STACK-REFERENCE 1
.with_highlighter(Box::<NoOpHighlighter>::default()) .with_highlighter(Box::<NoOpHighlighter>::default())
// CLEAR STACK-REFERENCE 2 // CLEAR STACK-REFERENCE 2
.with_completer(Box::<DefaultCompleter>::default()); .with_completer(Box::<DefaultCompleter>::default())
// Ensure immediately accept is always cleared
.with_immediately_accept(false);
// Let's grab the shell_integration configs // Let's grab the shell_integration configs
let shell_integration_osc2 = config.shell_integration.osc2; let shell_integration_osc2 = config.shell_integration.osc2;
@@ -671,7 +673,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
run_shell_integration_reset_application_mode(); run_shell_integration_reset_application_mode();
} }
flush_engine_state_repl_buffer(engine_state, &mut line_editor); line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
} }
Ok(Signal::CtrlC) => { Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown // `Reedline` clears the line content. New prompt is shown
@@ -1126,7 +1128,10 @@ fn run_shell_integration_reset_application_mode() {
/// ///
/// Clear the screen and output anything remaining in the EngineState buffer. /// Clear the screen and output anything remaining in the EngineState buffer.
/// ///
fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &mut Reedline) { fn flush_engine_state_repl_buffer(
engine_state: &mut EngineState,
mut line_editor: Reedline,
) -> Reedline {
let mut repl = engine_state.repl_state.lock().expect("repl state mutex"); let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
line_editor.run_edit_commands(&[ line_editor.run_edit_commands(&[
EditCommand::Clear, EditCommand::Clear,
@@ -1136,8 +1141,13 @@ fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &
select: false, select: false,
}, },
]); ]);
if repl.accept {
line_editor = line_editor.with_immediately_accept(true)
}
repl.accept = false;
repl.buffer = "".to_string(); repl.buffer = "".to_string();
repl.cursor_pos = 0; repl.cursor_pos = 0;
line_editor
} }
/// ///

View File

@@ -46,6 +46,8 @@ pub struct ReplState {
pub buffer: String, pub buffer: String,
// A byte position, as `EditCommand::MoveToPosition` is also a byte position // A byte position, as `EditCommand::MoveToPosition` is also a byte position
pub cursor_pos: usize, pub cursor_pos: usize,
/// Immediately accept the buffer on the next loop.
pub accept: bool,
} }
pub struct IsDebugging(AtomicBool); pub struct IsDebugging(AtomicBool);
@@ -185,6 +187,7 @@ impl EngineState {
repl_state: Arc::new(Mutex::new(ReplState { repl_state: Arc::new(Mutex::new(ReplState {
buffer: "".to_string(), buffer: "".to_string(),
cursor_pos: 0, cursor_pos: 0,
accept: false,
})), })),
table_decl_id: None, table_decl_id: None,
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
@@ -1078,6 +1081,7 @@ impl EngineState {
self.repl_state = Arc::new(Mutex::new(ReplState { self.repl_state = Arc::new(Mutex::new(ReplState {
buffer: "".to_string(), buffer: "".to_string(),
cursor_pos: 0, cursor_pos: 0,
accept: false,
})); }));
} }
if Mutex::is_poisoned(&self.jobs) { if Mutex::is_poisoned(&self.jobs) {

View File

@@ -140,3 +140,11 @@ fn commandline_test_cursor_end() -> TestResult {
fn commandline_test_cursor_type() -> TestResult { fn commandline_test_cursor_type() -> TestResult {
run_test("commandline get-cursor | describe", "int") run_test("commandline get-cursor | describe", "int")
} }
#[test]
fn commandline_test_accepted_command() -> TestResult {
run_test(
"commandline edit --accept \"print accepted\"\n | commandline",
"print accepted",
)
}