@@ -231,6 +231,8 @@ impl Command {
231231 select_mode, "Enter selection extend mode",
232232 exit_select_mode, "Exit selection mode",
233233 goto_definition, "Goto definition",
234+ add_newline_above, "Add newline above",
235+ add_newline_below, "Add newline below",
234236 goto_type_definition, "Goto type definition",
235237 goto_implementation, "Goto implementation",
236238 goto_file_start, "Goto file start/line",
@@ -1850,7 +1852,11 @@ mod cmd {
18501852 args: &[&str],
18511853 _event: PromptEvent,
18521854 ) -> anyhow::Result<()> {
1853− let dir = args.first().context("target directory not provided")?;
1855+ let dir = helix_core::path::expand_tilde(
1856+ args.first()
1857+ .context("target directory not provided")?
1858+ .as_ref(),
1859+ );
18541860
18551861 if let Err(e) = std::env::set_current_dir(dir) {
18561862 bail!("Couldn't change the current working directory: {}", e);
@@ -4494,7 +4500,6 @@ enum ShellBehavior {
44944500 Ignore,
44954501 Insert,
44964502 Append,
4497− Filter,
44984503}
44994504
45004505fn shell_pipe(cx: &mut Context) {
@@ -4514,18 +4519,101 @@ fn shell_append_output(cx: &mut Context) {
45144519}
45154520
45164521fn shell_keep_pipe(cx: &mut Context) {
4517− shell(cx, "keep-pipe:".into(), ShellBehavior::Filter);
4522+ let prompt = Prompt::new(
4523+ "keep-pipe:".into(),
4524+ Some('|'),
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 {
4529+ return;
4530+ }
4531+ if input.is_empty() {
4532+ return;
4533+ }
4534+ let (view, doc) = current!(cx.editor);
4535+ let selection = doc.selection(view.id);
4536+
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(..);
4541+
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,
4546+ Err(err) => {
4547+ cx.editor.set_error(err.to_string());
4548+ return;
4549+ }
4550+ };
4551+
4552+ // if the process exits successfully, keep the selection
4553+ if success {
4554+ ranges.push(*range);
4555+ if i >= old_index && index.is_none() {
4556+ index = Some(ranges.len() - 1);
4557+ }
4558+ }
4559+ }
4560+
4561+ if ranges.is_empty() {
4562+ cx.editor.set_error("No selections remaining".to_string());
4563+ return;
4564+ }
4565+
4566+ let index = index.unwrap_or_else(|| ranges.len() - 1);
4567+ doc.set_selection(view.id, Selection::new(ranges, index));
4568+ },
4569+ );
4570+
4571+ cx.push_layer(Box::new(prompt));
45184572}
45194573
4520−fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
4574+fn shell_impl(
4575+ shell: &[String],
4576+ cmd: &str,
4577+ input: Option<&[u8]>,
4578+) -> anyhow::Result<(Tendril, bool)> {
45214579 use std::io::Write;
45224580 use std::process::{Command, Stdio};
4523− if cx.editor.config.shell.is_empty() {
4524− cx.editor.set_error("No shell set".to_owned());
4525− return;
4581+ if shell.is_empty() {
4582+ bail!("No shell set");
4583+ }
4584+
4585+ let mut process = match Command::new(&shell[0])
4586+ .args(&shell[1..])
4587+ .arg(cmd)
4588+ .stdin(Stdio::piped())
4589+ .stdout(Stdio::piped())
4590+ .stderr(Stdio::piped())
4591+ .spawn()
4592+ {
4593+ Ok(process) => process,
4594+ Err(e) => {
4595+ log::error!("Failed to start shell: {}", e);
4596+ return Err(e.into());
4597+ }
4598+ };
4599+ if let Some(input) = input {
4600+ let mut stdin = process.stdin.take().unwrap();
4601+ stdin.write_all(input)?;
4602+ }
4603+ let output = process.wait_with_output()?;
4604+
4605+ if !output.stderr.is_empty() {
4606+ log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
45264607 }
4608+
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()))
4612+}
4613+
4614+fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
45274615 let pipe = match behavior {
4528− ShellBehavior::Replace | ShellBehavior::Ignore | ShellBehavior::Filter => true,
4616+ ShellBehavior::Replace | ShellBehavior::Ignore => true,
45294617 ShellBehavior::Insert | ShellBehavior::Append => false,
45304618 };
45314619 let prompt = Prompt::new(
@@ -4533,70 +4621,42 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
45334621 Some('|'),
45344622 |_input: &str| Vec::new(),
45354623 move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
4624+ let shell = &cx.editor.config.shell;
45364625 if event != PromptEvent::Validate {
45374626 return;
45384627 }
4539− let shell = &cx.editor.config.shell;
4628+ if input.is_empty() {
4629+ return;
4630+ }
45404631 let (view, doc) = current!(cx.editor);
45414632 let selection = doc.selection(view.id);
45424633
45434634 let mut changes = Vec::with_capacity(selection.len());
4635+ let text = doc.text().slice(..);
45444636
45454637 for range in selection.ranges() {
4546− let mut process = match Command::new(&shell[0])
4547− .args(&shell[1..])
4548− .arg(input)
4549− .stdin(Stdio::piped())
4550− .stdout(Stdio::piped())
4551− .stderr(Stdio::piped())
4552− .spawn()
4553− {
4554− Ok(process) => process,
4555− Err(e) => {
4556− log::error!("Failed to start shell: {}", e);
4557− cx.editor.set_error("Failed to start shell".to_owned());
4558− return;
4559− }
4560− };
4561− if pipe {
4562− let stdin = process.stdin.as_mut().unwrap();
4563− let fragment = range.fragment(doc.text().slice(..));
4564− stdin.write_all(fragment.as_bytes()).unwrap();
4565− }
4566− let output = process.wait_with_output().unwrap();
4567−
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));
4572− }
4573− cx.editor.set_error("Command failed".to_owned());
4574− return;
4575− }
4576− let tendril = match Tendril::try_from_byte_slice(&output.stdout) {
4577− Ok(tendril) => tendril,
4578− Err(_) => {
4579− cx.editor
4580− .set_error("Process did not output valid UTF-8".to_owned());
4638+ let fragment = range.fragment(text);
4639+ let (output, success) =
4640+ match shell_impl(shell, input, pipe.then(|| fragment.as_bytes())) {
4641+ Ok(result) => result,
4642+ Err(err) => {
4643+ cx.editor.set_error(err.to_string());
45814644 return;
45824645 }
45834646 };
4584− let (from, to) = match behavior {
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()),
4589− };
4590− changes.push((from, to, Some(tendril)));
4591− } else {
4592− // if the process exits successfully, keep the selection, otherwise delete it.
4593− let keep = output.status.success();
4594− changes.push((
4595− range.from(),
4596− if keep { range.from() } else { range.to() },
4597− None,
4598− ));
4647+
4648+ if !success {
4649+ cx.editor.set_error("Command failed".to_string());
4650+ return;
45994651 }
4652+
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()),
4658+ };
4659+ changes.push((from, to, Some(output)));
46004660 }
46014661
46024662 if behavior != ShellBehavior::Ignore {
@@ -4614,3 +4674,37 @@ fn suspend(_cx: &mut Context) {
46144674 #[cfg(not(windows))]
46154675 signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
46164676}
4677+
4678+fn add_newline_above(cx: &mut Context) {
4679+ add_newline_impl(cx, Open::Above);
4680+}
4681+
4682+fn add_newline_below(cx: &mut Context) {
4683+ add_newline_impl(cx, Open::Below)
4684+}
4685+
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(..);
4692+
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,
4698+ };
4699+ let pos = text.line_to_char(line);
4700+ (
4701+ pos,
4702+ pos,
4703+ Some(doc.line_ending.as_str().repeat(count).into()),
4704+ )
4705+ });
4706+
4707+ let transaction = Transaction::change(text, changes);
4708+ doc.apply(&transaction, view.id);
4709+ doc.append_changes_to_history(view.id);
4710+}