@@ -231,6 +231,8 @@ impl Command {
231 select_mode, "Enter selection extend mode",
231 select_mode, "Enter selection extend mode",
232 exit_select_mode, "Exit selection mode",
232 exit_select_mode, "Exit selection mode",
233 goto_definition, "Goto definition",
233 goto_definition, "Goto definition",
234+ add_newline_above, "Add newline above",
235+ add_newline_below, "Add newline below",
234 goto_type_definition, "Goto type definition",
236 goto_type_definition, "Goto type definition",
235 goto_implementation, "Goto implementation",
237 goto_implementation, "Goto implementation",
236 goto_file_start, "Goto file start/line",
238 goto_file_start, "Goto file start/line",
@@ -1850,7 +1852,11 @@ mod cmd {
1850 args: &[&str],
1852 args: &[&str],
1851 _event: PromptEvent,
1853 _event: PromptEvent,
1852 ) -> anyhow::Result<()> {
1854 ) -> anyhow::Result<()> {
1855+ let dir = helix_core::path::expand_tilde(
1853− let dir = args.first().context("target directory not provided")?;
1857+ .context("target directory not provided")?
1855 if let Err(e) = std::env::set_current_dir(dir) {
1861 if let Err(e) = std::env::set_current_dir(dir) {
1856 bail!("Couldn't change the current working directory: {}", e);
1862 bail!("Couldn't change the current working directory: {}", e);
@@ -4494,7 +4500,6 @@ enum ShellBehavior {
4500fn shell_pipe(cx: &mut Context) {
4505fn shell_pipe(cx: &mut Context) {
@@ -4514,18 +4519,101 @@ fn shell_append_output(cx: &mut Context) {
4516fn shell_keep_pipe(cx: &mut Context) {
4521fn shell_keep_pipe(cx: &mut Context) {
4522+ let prompt = Prompt::new(
4517− shell(cx, "keep-pipe:".into(), ShellBehavior::Filter);
4523+ "keep-pipe:".into(),
4525+ |_input: &str| Vec::new(),
4526+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4527+ let shell = &cx.editor.config.shell;
4528+ if event != PromptEvent::Validate {
4531+ if input.is_empty() {
4534+ let (view, doc) = current!(cx.editor);
4535+ let selection = doc.selection(view.id);
4537+ let mut ranges = SmallVec::with_capacity(selection.len());
4538+ let old_index = selection.primary_index();
4539+ let mut index: Option<usize> = None;
4540+ let text = doc.text().slice(..);
4542+ for (i, range) in selection.ranges().iter().enumerate() {
4543+ let fragment = range.fragment(text);
4544+ let (_output, success) = match shell_impl(shell, input, Some(fragment.as_bytes())) {
4545+ Ok(result) => result,
4547+ cx.editor.set_error(err.to_string());
4552+ // if the process exits successfully, keep the selection
4554+ ranges.push(*range);
4555+ if i >= old_index && index.is_none() {
4556+ index = Some(ranges.len() - 1);
4561+ if ranges.is_empty() {
4562+ cx.editor.set_error("No selections remaining".to_string());
4566+ let index = index.unwrap_or_else(|| ranges.len() - 1);
4567+ doc.set_selection(view.id, Selection::new(ranges, index));
4571+ cx.push_layer(Box::new(prompt));
4520−fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4574+fn shell_impl(
4577+ input: Option<&[u8]>,
4578+) -> anyhow::Result<(Tendril, bool)> {
4521 use std::io::Write;
4579 use std::io::Write;
4522 use std::process::{Command, Stdio};
4580 use std::process::{Command, Stdio};
4523− if cx.editor.config.shell.is_empty() {
4581+ if shell.is_empty() {
4524− cx.editor.set_error("No shell set".to_owned());
4582+ bail!("No shell set");
4585+ let mut process = match Command::new(&shell[0])
4588+ .stdin(Stdio::piped())
4589+ .stdout(Stdio::piped())
4590+ .stderr(Stdio::piped())
4593+ Ok(process) => process,
4595+ log::error!("Failed to start shell: {}", e);
4525− return;
4596+ return Err(e.into());
4599+ if let Some(input) = input {
4600+ let mut stdin = process.stdin.take().unwrap();
4601+ stdin.write_all(input)?;
4603+ let output = process.wait_with_output()?;
4605+ if !output.stderr.is_empty() {
4606+ log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
4609+ let tendril = Tendril::try_from_byte_slice(&output.stdout)
4610+ .map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
4611+ Ok((tendril, output.status.success()))
4614+fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4527 let pipe = match behavior {
4615 let pipe = match behavior {
4528− ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
4616+ ShellBehavior::Replace | ShellBehavior::Ignore => true,
4529 ShellBehavior::Insert | ShellBehavior::Append => false,
4617 ShellBehavior::Insert | ShellBehavior::Append => false,
4531 let prompt = Prompt::new(
4619 let prompt = Prompt::new(
@@ -4533,70 +4621,42 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4533 Some('|'),
4621 Some('|'),
4534 |_input: &str| Vec::new(),
4622 |_input: &str| Vec::new(),
4535 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4623 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4624+ let shell = &cx.editor.config.shell;
4536 if event != PromptEvent::Validate {
4625 if event != PromptEvent::Validate {
4539− let shell = &cx.editor.config.shell;
4628+ if input.is_empty() {
4540 let (view, doc) = current!(cx.editor);
4631 let (view, doc) = current!(cx.editor);
4541 let selection = doc.selection(view.id);
4632 let selection = doc.selection(view.id);
4543 let mut changes = Vec::with_capacity(selection.len());
4634 let mut changes = Vec::with_capacity(selection.len());
4635+ let text = doc.text().slice(..);
4545 for range in selection.ranges() {
4637 for range in selection.ranges() {
4546− let mut process = match Command::new(&shell[0])
4549− .stdin(Stdio::piped())
4550− .stdout(Stdio::piped())
4551− .stderr(Stdio::piped())
4554− Ok(process) => process,
4556− log::error!("Failed to start shell: {}", e);
4557− cx.editor.set_error("Failed to start shell".to_owned());
4562− let stdin = process.stdin.as_mut().unwrap();
4563− let fragment = range.fragment(doc.text().slice(..));
4638+ let fragment = range.fragment(text);
4564− stdin.write_all(fragment.as_bytes()).unwrap();
4639+ let (output, success) =
4565− }
4640+ match shell_impl(shell, input, pipe.then(|| fragment.as_bytes())) {
4566− let output = process.wait_with_output().unwrap();
4641+ Ok(result) => result,
4568− if behavior != ShellBehavior::Filter {
4569− if !output.status.success() {
4570− if !output.stderr.is_empty() {
4571− log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
4573− cx.editor.set_error("Command failed".to_owned());
4576− let tendril = match Tendril::try_from_byte_slice(&output.stdout) {
4577− Ok(tendril) => tendril,
4578− Err(_) => {
4642+ Err(err) => {
4579− cx.editor
4643+ cx.editor.set_error(err.to_string());
4580− .set_error("Process did not output valid UTF-8".to_owned());
4584− let (from, to) = match behavior {
4647+
4585− ShellBehavior::Replace => (range.from(), range.to()),
4586− ShellBehavior::Insert => (range.from(), range.from()),
4587− ShellBehavior::Append => (range.to(), range.to()),
4588− _ => (range.from(), range.from()),
4590− changes.push((from, to, Some(tendril)));
4592− // if the process exits successfully, keep the selection, otherwise delete it.
4593− let keep = output.status.success();
4648+ if !success {
4594− changes.push((
4649+ cx.editor.set_error("Command failed".to_string());
4595− range.from(),
4650+ return;
4596− if keep { range.from() } else { range.to() },
4653+ let (from, to) = match behavior {
4654+ ShellBehavior::Replace => (range.from(), range.to()),
4655+ ShellBehavior::Insert => (range.from(), range.from()),
4656+ ShellBehavior::Append => (range.to(), range.to()),
4657+ _ => (range.from(), range.from()),
4659+ changes.push((from, to, Some(output)));
4602 if behavior != ShellBehavior::Ignore {
4662 if behavior != ShellBehavior::Ignore {
@@ -4614,3 +4674,37 @@ fn suspend(_cx: &mut Context) {
4614 #[cfg(not(windows))]
4674 #[cfg(not(windows))]
4615 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
4675 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
4678+fn add_newline_above(cx: &mut Context) {
4679+ add_newline_impl(cx, Open::Above);
4682+fn add_newline_below(cx: &mut Context) {
4683+ add_newline_impl(cx, Open::Below)
4686+fn add_newline_impl(cx: &mut Context, open: Open) {
4687+ let count = cx.count();
4688+ let (view, doc) = current!(cx.editor);
4689+ let selection = doc.selection(view.id);
4690+ let text = doc.text();
4691+ let slice = text.slice(..);
4693+ let changes = selection.into_iter().map(|range| {
4694+ let (start, end) = range.line_range(slice);
4695+ let line = match open {
4696+ Open::Above => start,
4697+ Open::Below => end + 1,
4699+ let pos = text.line_to_char(line);
4703+ Some(doc.line_ending.as_str().repeat(count).into()),
4707+ let transaction = Transaction::change(text, changes);
4708+ doc.apply(&transaction, view.id);
4709+ doc.append_changes_to_history(view.id);