aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs153
-rw-r--r--helix-term/src/keymap.rs5
-rw-r--r--helix-term/src/ui/editor.rs34
-rw-r--r--helix-term/src/ui/mod.rs2
-rw-r--r--helix-term/src/ui/picker.rs2
-rw-r--r--helix-term/src/ui/prompt.rs4
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), &regex)
{
@@ -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), &regex);
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, &regex, 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, &regex, 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), &regex) {
@@ -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,