feat: run shell commands asynchronously from command mode

This commit is contained in:
2026-10-18 00:30:24 +02:00
parent f942f881e6
commit a5f92452b3
16 changed files with 817 additions and 13 deletions
+71
View File
@@ -4,6 +4,77 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
};
#[cfg(unix)]
#[test]
fn shell_commands_leave_editing_live_and_present_both_streams_and_exit_status() {
let fixture = Fixture::new();
fixture.file("buffer.txt", "source");
let mut session = fixture.open("");
session.editor.set_project_root(fixture.0.clone());
for ch in
":!while [ ! -f release ]; do sleep 0.01; done; pwd; printf 'out'; printf 'err' >&2; exit 7"
.chars()
{
session.handle(Input::Char(ch));
}
session.handle(Input::Enter);
let fixed = Instant::now();
session.shell.as_mut().unwrap().2 = fixed;
assert!(session.poll_shell_at(fixed + Duration::from_millis(100)));
assert_eq!(session.editor.shell_label(), "whi... /");
session.execute("!printf should-not-run > unexpected");
assert_eq!(
session.editor.message(),
"A shell command is already running"
);
assert!(!fixture.0.join("unexpected").exists());
session.handle(Input::Char('i'));
session.handle(Input::Char('X'));
assert_eq!(session.editor.lines(), ["Xsource"]);
fixture.file("release", "");
drain_until(&mut session, |session| {
session.editor.command_output().is_some()
});
let output = session.editor.command_output().unwrap();
assert!(output.title.contains("exit 7"));
let text = output.content.lines.join("\n");
assert!(text.contains(fixture.0.to_str().unwrap()));
assert!(text.ends_with("out\n[stderr]\nerr"), "{text}");
assert_eq!(session.editor.shell_label(), "");
assert_eq!(session.editor.mode(), Mode::Insert);
session.handle(Input::Char('Z'));
assert_eq!(session.editor.lines(), ["Xsource"]);
session.handle(Input::Escape);
session.handle(Input::Char('Y'));
assert_eq!(session.editor.lines(), ["XYsource"]);
assert_eq!(
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
"source"
);
}
#[cfg(unix)]
#[test]
fn shell_commands_reject_empty_input_and_show_empty_success_without_editing() {
let fixture = Fixture::new();
fixture.file("buffer.txt", "source");
let mut session = fixture.open("");
session.execute(":!");
assert_eq!(session.editor.message(), "Shell command is empty");
assert!(session.shell.is_none());
session.execute(":!true");
drain_until(&mut session, |session| {
session.editor.command_output().is_some()
});
let output = session.editor.command_output().unwrap();
assert_eq!(output.content.lines, ["(no output)"]);
assert!(output.title.contains("exit 0"));
session.handle(Input::Enter);
assert!(session.editor.command_output().is_none());
assert_eq!(session.editor.lines(), ["source"]);
assert!(!session.editor.dirty());
}
#[test]
fn oversized_repository_text_skips_snapshots_and_recovers_after_shrinking() {
let fixture = Fixture::new();