feat: run shell commands asynchronously from command mode
This commit is contained in:
+128
@@ -137,6 +137,11 @@ pub struct StartupDiagnostic {
|
||||
max_column: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct ShellOutput {
|
||||
pub(crate) title: String,
|
||||
pub(crate) content: StartupDiagnostic,
|
||||
}
|
||||
|
||||
pub struct Hover {
|
||||
text: String,
|
||||
line_starts: Vec<usize>,
|
||||
@@ -2223,6 +2228,8 @@ pub struct Editor {
|
||||
language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
|
||||
battery_charge: Option<u8>,
|
||||
battery_label: String,
|
||||
shell_label: String,
|
||||
command_output: Option<ShellOutput>,
|
||||
git_blame: bool,
|
||||
gutter_style: crate::gutter::GutterStyle,
|
||||
line_blame: Option<(BlameSource, Arc<crate::blame::LineBlame>, String)>,
|
||||
@@ -2339,6 +2346,8 @@ impl Editor {
|
||||
language_tab_spaces: BTreeMap::new(),
|
||||
battery_charge: None,
|
||||
battery_label: String::new(),
|
||||
shell_label: String::new(),
|
||||
command_output: None,
|
||||
git_blame: true,
|
||||
gutter_style: crate::gutter::GutterStyle::default(),
|
||||
line_blame: None,
|
||||
@@ -4698,6 +4707,7 @@ impl Editor {
|
||||
&& self.document_actions_available()
|
||||
&& self.startup_diagnostic.is_none()
|
||||
&& self.hover.is_none()
|
||||
&& self.command_output.is_none()
|
||||
&& self.palette.is_none()
|
||||
&& self.completion.is_none()
|
||||
&& !self.file_picker().is_some_and(|picker| picker.focused)
|
||||
@@ -6914,6 +6924,107 @@ impl Editor {
|
||||
pub fn battery_label(&self) -> &str {
|
||||
&self.battery_label
|
||||
}
|
||||
pub(crate) fn shell_label(&self) -> &str {
|
||||
&self.shell_label
|
||||
}
|
||||
|
||||
pub(crate) fn set_shell_label(&mut self, label: String) -> bool {
|
||||
if self.shell_label == label {
|
||||
return false;
|
||||
}
|
||||
self.shell_label = label;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn command_output(&self) -> Option<&ShellOutput> {
|
||||
self.command_output.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn show_command_output(
|
||||
&mut self,
|
||||
command: String,
|
||||
result: Result<crate::shell::Output, String>,
|
||||
) {
|
||||
let (status, text) = match result {
|
||||
Ok(output) => {
|
||||
let status = output.code.map_or_else(
|
||||
|| "terminated by signal".into(),
|
||||
|code| format!("exit {code}"),
|
||||
);
|
||||
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||
if !output.stderr.is_empty() {
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push_str("[stderr]\n");
|
||||
text.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
if text.is_empty() {
|
||||
text.push_str("(no output)");
|
||||
}
|
||||
(status, text)
|
||||
}
|
||||
Err(error) => ("failed".into(), error),
|
||||
};
|
||||
// Captured bytes are text, never terminal escape sequences.
|
||||
let lines: Vec<String> = normalize_newlines(&text)
|
||||
.split('\n')
|
||||
.map(|line| {
|
||||
line.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_control() && ch != '\t' {
|
||||
'�'
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let max_column = lines
|
||||
.iter()
|
||||
.map(|line| grapheme_count(line).saturating_sub(1))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let command: String = command
|
||||
.chars()
|
||||
.map(|ch| if ch.is_control() { '�' } else { ch })
|
||||
.collect();
|
||||
self.command_output = Some(ShellOutput {
|
||||
title: format!("{status} · !{command}"),
|
||||
content: StartupDiagnostic {
|
||||
lines,
|
||||
top: 0,
|
||||
left: 0,
|
||||
max_column,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_command_output(&mut self, input: &Input) -> bool {
|
||||
let Some(output) = &mut self.command_output else {
|
||||
return false;
|
||||
};
|
||||
let content = &mut output.content;
|
||||
match input {
|
||||
Input::Escape | Input::Enter => self.command_output = None,
|
||||
Input::Up | Input::Ctrl('k') => content.top = content.top.saturating_sub(1),
|
||||
Input::Down | Input::Ctrl('j') => {
|
||||
content.top = content.top.saturating_add(1).min(content.lines.len() - 1);
|
||||
}
|
||||
Input::Left | Input::Ctrl('h') => content.left = content.left.saturating_sub(1),
|
||||
Input::Right | Input::Ctrl('l') => {
|
||||
content.left = content.left.saturating_add(1).min(content.max_column);
|
||||
}
|
||||
Input::Home => {
|
||||
content.top = 0;
|
||||
content.left = 0;
|
||||
}
|
||||
Input::End => content.top = content.lines.len() - 1,
|
||||
_ => {}
|
||||
}
|
||||
true
|
||||
}
|
||||
pub(crate) fn gutter_style(&self) -> crate::gutter::GutterStyle {
|
||||
self.gutter_style
|
||||
}
|
||||
@@ -7975,6 +8086,9 @@ impl Editor {
|
||||
Input::Ctrl(ch) => control_key(ch),
|
||||
input => input,
|
||||
};
|
||||
if self.handle_command_output(&input) {
|
||||
return None;
|
||||
}
|
||||
if self.handle_review_overview(&input) {
|
||||
return None;
|
||||
}
|
||||
@@ -9544,6 +9658,20 @@ impl Editor {
|
||||
|
||||
fn execute_command(&mut self, command: &str) -> Option<Invocation> {
|
||||
let command = command.trim().strip_prefix(':').unwrap_or(command.trim());
|
||||
if let Some(command) = command.strip_prefix('!') {
|
||||
self.prepare_command();
|
||||
self.active.view.command_line.clear();
|
||||
self.finish_command_mode();
|
||||
if command.trim().is_empty() {
|
||||
self.set_message("Shell command is empty");
|
||||
return None;
|
||||
}
|
||||
return Some(Invocation {
|
||||
plugin: "core".into(),
|
||||
command: "shell".into(),
|
||||
args: command.into(),
|
||||
});
|
||||
}
|
||||
let (name, args) = command
|
||||
.split_once(char::is_whitespace)
|
||||
.unwrap_or((command, ""));
|
||||
|
||||
Reference in New Issue
Block a user