diff options
Diffstat (limited to 'helix-term/src')
-rw-r--r-- | helix-term/src/commands.rs | 153 | ||||
-rw-r--r-- | helix-term/src/keymap.rs | 5 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 34 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 4 |
6 files changed, 164 insertions, 36 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 860cacc6..4ff47574 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -135,7 +135,7 @@ pub struct Command { } macro_rules! commands { - ( $($name:ident, $doc:literal),* ) => { + ( $($name:ident, $doc:literal,)* ) => { $( #[allow(non_upper_case_globals)] pub const $name: Self = Self { @@ -317,7 +317,12 @@ impl Command { dap_variables, "List variables", dap_terminate, "End debug session", dap_switch_thread, "Switch current thread", - suspend, "Suspend" + shell_pipe, "Pipe selections through shell command", + shell_pipe_to, "Pipe selections into shell command, ignoring command output", + shell_insert_output, "Insert output of shell command before each selection", + shell_append_output, "Append output of shell command after each selection", + shell_keep_pipe, "Filter selections with shell predicate", + suspend, "Suspend", ); } @@ -1089,7 +1094,7 @@ fn select_all(cx: &mut Context) { } fn select_regex(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "select:".to_string(), move |view, doc, _, regex| { + let prompt = ui::regex_prompt(cx, "select:".into(), move |view, doc, _, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::select_on_matches(text, doc.selection(view.id), ®ex) { @@ -1101,7 +1106,7 @@ fn select_regex(cx: &mut Context) { } fn split_selection(cx: &mut Context) { - let prompt = ui::regex_prompt(cx, "split:".to_string(), move |view, doc, _, regex| { + let prompt = ui::regex_prompt(cx, "split:".into(), move |view, doc, _, regex| { let text = doc.text().slice(..); let selection = selection::split_on_matches(text, doc.selection(view.id), ®ex); doc.set_selection(view.id, selection); @@ -1167,15 +1172,11 @@ fn search(cx: &mut Context) { // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); - let prompt = ui::regex_prompt( - cx, - "search:".to_string(), - move |view, doc, registers, regex| { - search_impl(doc, view, &contents, ®ex, false); - // TODO: only store on enter (accept), not update - registers.write('/', vec![regex.as_str().to_string()]); - }, - ); + let prompt = ui::regex_prompt(cx, "search:".into(), move |view, doc, registers, regex| { + search_impl(doc, view, &contents, ®ex, false); + // TODO: only store on enter (accept), not update + registers.write('/', vec![regex.as_str().to_string()]); + }); cx.push_layer(Box::new(prompt)); } @@ -2400,7 +2401,7 @@ mod cmd { fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( - ":".to_owned(), + ":".into(), Some(':'), |input: &str| { // we use .this over split_whitespace() because we care about empty segments @@ -4009,7 +4010,7 @@ fn join_selections(cx: &mut Context) { fn keep_selections(cx: &mut Context) { // keep selections matching regex - let prompt = ui::regex_prompt(cx, "keep:".to_string(), move |view, doc, _, regex| { + let prompt = ui::regex_prompt(cx, "keep:".into(), move |view, doc, _, regex| { let text = doc.text().slice(..); if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { @@ -4487,6 +4488,128 @@ fn surround_delete(cx: &mut Context) { }) } +#[derive(Eq, PartialEq)] +enum ShellBehavior { + Replace, + Ignore, + Insert, + Append, + Filter, +} + +fn shell_pipe(cx: &mut Context) { + shell(cx, "pipe:".into(), ShellBehavior::Replace); +} + +fn shell_pipe_to(cx: &mut Context) { + shell(cx, "pipe-to:".into(), ShellBehavior::Ignore); +} + +fn shell_insert_output(cx: &mut Context) { + shell(cx, "insert-output:".into(), ShellBehavior::Insert); +} + +fn shell_append_output(cx: &mut Context) { + shell(cx, "append-output:".into(), ShellBehavior::Append); +} + +fn shell_keep_pipe(cx: &mut Context) { + shell(cx, "keep-pipe:".into(), ShellBehavior::Filter); +} + +fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { + use std::io::Write; + use std::process::{Command, Stdio}; + if cx.editor.config.shell.is_empty() { + cx.editor.set_error("No shell set".to_owned()); + return; + } + let pipe = match behavior { + ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true, + ShellBehavior::Insert | ShellBehavior::Append => false, + }; + let prompt = Prompt::new( + prompt, + Some('|'), + |_input: &str| Vec::new(), + move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + let shell = &cx.editor.config.shell; + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); + + let mut changes = Vec::with_capacity(selection.len()); + + for range in selection.ranges() { + let mut process = match Command::new(&shell[0]) + .args(&shell[1..]) + .arg(input) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(process) => process, + Err(e) => { + log::error!("Failed to start shell: {}", e); + cx.editor.set_error("Failed to start shell".to_owned()); + return; + } + }; + if pipe { + let stdin = process.stdin.as_mut().unwrap(); + let fragment = range.fragment(doc.text().slice(..)); + stdin.write_all(fragment.as_bytes()).unwrap(); + } + let output = process.wait_with_output().unwrap(); + + if behavior != ShellBehavior::Filter { + if !output.status.success() { + if !output.stderr.is_empty() { + log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); + } + cx.editor.set_error("Command failed".to_owned()); + return; + } + let tendril = match Tendril::try_from_byte_slice(&output.stdout) { + Ok(tendril) => tendril, + Err(_) => { + cx.editor + .set_error("Process did not output valid UTF-8".to_owned()); + return; + } + }; + let (from, to) = match behavior { + ShellBehavior::Replace => (range.from(), range.to()), + ShellBehavior::Insert => (range.from(), range.from()), + ShellBehavior::Append => (range.to(), range.to()), + _ => (range.from(), range.from()), + }; + changes.push((from, to, Some(tendril))); + } else { + // if the process exits successfully, keep the selection, otherwise delete it. + let keep = output.status.success(); + changes.push(( + range.from(), + if keep { range.from() } else { range.to() }, + None, + )); + } + } + + if behavior != ShellBehavior::Ignore { + let transaction = Transaction::change(doc.text(), changes.into_iter()); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + } + }, + ); + + cx.push_layer(Box::new(prompt)); +} + fn suspend(_cx: &mut Context) { #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 5a26529f..706cf29f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -525,6 +525,11 @@ impl Default for Keymaps { }, "\"" => select_register, + "|" => shell_pipe, + "A-|" => shell_pipe_to, + "!" => shell_insert_output, + "A-!" => shell_append_output, + "$" => shell_keep_pipe, "C-z" => suspend, }); let mut select = normal.clone(); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 725d58b3..e661c5a0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -155,21 +155,21 @@ impl EditorView { syntax .highlight_iter(text.slice(..), Some(range), None, |language| { loader - .language_config_for_scope(&format!("source.{}", language)) - .and_then(|language_config| { - let config = language_config.highlight_config(scopes)?; - let config_ref = config.as_ref(); - // SAFETY: the referenced `HighlightConfiguration` behind - // the `Arc` is guaranteed to remain valid throughout the - // duration of the highlight. - let config_ref = unsafe { - std::mem::transmute::< - _, - &'static syntax::HighlightConfiguration, - >(config_ref) - }; - Some(config_ref) - }) + .language_config_for_scope(&format!("source.{}", language)) + .and_then(|language_config| { + let config = language_config.highlight_config(scopes)?; + let config_ref = config.as_ref(); + // SAFETY: the referenced `HighlightConfiguration` behind + // the `Arc` is guaranteed to remain valid throughout the + // duration of the highlight. + let config_ref = unsafe { + std::mem::transmute::< + _, + &'static syntax::HighlightConfiguration, + >(config_ref) + }; + Some(config_ref) + }) }) .map(|event| event.unwrap()) .collect() // TODO: we collect here to avoid holding the lock, fix later @@ -435,7 +435,7 @@ impl EditorView { let current_line = doc .text() - .char_to_line(doc.selection(view.id).primary().anchor); + .char_to_line(doc.selection(view.id).primary().cursor(text)); // it's used inside an iterator so the collect isn't needless: // https://github.com/rust-lang/rust-clippy/issues/6164 @@ -749,7 +749,7 @@ impl EditorView { _ => noop, }; Prompt::new( - format!("{}: ", name), + format!("{}: ", name).into(), None, completer, move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f3f8670e..0a1e24b5 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -27,7 +27,7 @@ use std::path::PathBuf; pub fn regex_prompt( cx: &mut crate::commands::Context, - prompt: String, + prompt: std::borrow::Cow<'static, str>, fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ef2c434c..06e424ea 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -202,7 +202,7 @@ impl<T> Picker<T> { callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( - "".to_string(), + "".into(), None, |_pattern: &str| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 7197adea..1d512ad2 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -15,7 +15,7 @@ use helix_view::{ pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub struct Prompt { - prompt: String, + prompt: Cow<'static, str>, pub line: String, cursor: usize, completion: Vec<Completion>, @@ -55,7 +55,7 @@ pub enum Movement { impl Prompt { pub fn new( - prompt: String, + prompt: Cow<'static, str>, history_register: Option<char>, mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, |