From 7eff9056802d3d4f67fbaef7ba7dc03e874564c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 29 Aug 2021 12:40:21 +0900 Subject: [PATCH 01/18] lsp: slightly refactor header parsing, add more logging --- helix-lsp/src/transport.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 5be694c82..068ea2300 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,7 +1,7 @@ use crate::Result; -use anyhow::Context; +use anyhow::{anyhow, Context}; use jsonrpc_core as jsonrpc; -use log::{error, info}; +use log::{debug, error, info}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -83,20 +83,16 @@ impl Transport { break; } - let mut parts = header.split(": "); + debug!("<- header {}", header); - match (parts.next(), parts.next(), parts.next()) { - (Some("Content-Length"), Some(value), None) => { + let parts = header.split_once(": "); + + match parts { + Some(("Content-Length", value)) => { content_length = Some(value.parse().context("invalid content length")?); } - (Some(_), Some(_), None) => {} - _ => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse header", - ) - .into()); - } + Some((_, _)) => {} + None => return Err(anyhow!("Failed to parse header: {:?}", header).into()), } } From 847d1fa496f157c1a9640c5cedd92d6593e33b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 29 Aug 2021 18:38:28 +0900 Subject: [PATCH 02/18] fix: Work around crashes on LSPs that don't just emit JSON-RPC --- helix-lsp/src/transport.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 068ea2300..67b7b48fa 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -1,7 +1,7 @@ use crate::Result; -use anyhow::{anyhow, Context}; +use anyhow::Context; use jsonrpc_core as jsonrpc; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -92,7 +92,12 @@ impl Transport { content_length = Some(value.parse().context("invalid content length")?); } Some((_, _)) => {} - None => return Err(anyhow!("Failed to parse header: {:?}", header).into()), + None => { + // Workaround: Some non-conformant language servers will output logging and other garbage + // into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn + // the server. Skip such lines and log a warning. + warn!("Failed to parse header: {:?}", header); + } } } From 03ad9e0bfa37222762683aeb84be2d324d166930 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 29 Aug 2021 20:15:49 -0400 Subject: [PATCH 03/18] Fix code indentation (#671) --- helix-term/src/ui/editor.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 72b8adc1d..de0d065eb 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -148,21 +148,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 From 4f8dc4cad862e430927b41e6dbffb22167057161 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Mon, 30 Aug 2021 10:58:22 +0200 Subject: [PATCH 04/18] Fix it's -> its (#676) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 199caee63..d596c83d0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ cargo install --path helix-term This will install the `hx` binary to `$HOME/.cargo/bin`. -Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the +Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden via the `HELIX_RUNTIME` environment variable. From b590504143412f185f02ee21a54ad4e6d966b257 Mon Sep 17 00:00:00 2001 From: gbaranski Date: Mon, 30 Aug 2021 20:28:40 +0200 Subject: [PATCH 05/18] fix: use head instead of anchor for relative line --- helix-term/src/ui/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index de0d065eb..2c34ae963 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -427,7 +427,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().head); // it's used inside an iterator so the collect isn't needless: // https://github.com/rust-lang/rust-clippy/issues/6164 From 9c5752cbac2450388ff4c94dfa82b389b3746a9c Mon Sep 17 00:00:00 2001 From: gbaranski Date: Mon, 30 Aug 2021 20:49:17 +0200 Subject: [PATCH 06/18] fix: use .cursor() instead of .head --- helix-term/src/ui/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 2c34ae963..4b9c56e76 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -427,7 +427,7 @@ impl EditorView { let current_line = doc .text() - .char_to_line(doc.selection(view.id).primary().head); + .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 From e57a00c19c962d6f9a537148fa140df5adf478fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:06:47 +0000 Subject: [PATCH 07/18] build(deps): bump thiserror from 1.0.26 to 1.0.28 Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.26 to 1.0.28. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.26...1.0.28) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7dbb9c83..9012a86c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,18 +930,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71" dependencies = [ "proc-macro2", "quote", From d7b2ac03810a02ac1ce41229a90badfbf2357a8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:06:58 +0000 Subject: [PATCH 08/18] build(deps): bump futures-util from 0.3.16 to 0.3.17 Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.16 to 0.3.17. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.16...0.3.17) --- updated-dependencies: - dependency-name: futures-util dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9012a86c2..9d6013d5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,9 +232,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" @@ -249,15 +249,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-core", From d1d6810560431091b5149fa1b3f228739d070c3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:07:09 +0000 Subject: [PATCH 09/18] build(deps): bump futures-executor from 0.3.16 to 0.3.17 Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.16 to 0.3.17. - [Release notes](https://github.com/rust-lang/futures-rs/releases) - [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.16...0.3.17) --- updated-dependencies: - dependency-name: futures-executor dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d6013d5f..15ef348ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,9 +238,9 @@ checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", From 9d83a4483d66deac956eb6c673bcb68ba33d71dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:07:19 +0000 Subject: [PATCH 10/18] build(deps): bump tokio from 1.10.0 to 1.10.1 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.10.0...tokio-1.10.1) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- helix-lsp/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15ef348ba..f7d8108f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -983,9 +983,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5" dependencies = [ "autocfg", "bytes", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 2d4a16c6d..52e995f5d 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.9", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio = { version = "1.10", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1.7" From 3ce578c1a1a93081b9d781718d51d5fb4d783062 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Aug 2021 23:07:28 +0000 Subject: [PATCH 11/18] build(deps): bump slotmap from 1.0.5 to 1.0.6 Bumps [slotmap](https://github.com/orlp/slotmap) from 1.0.5 to 1.0.6. - [Release notes](https://github.com/orlp/slotmap/releases) - [Changelog](https://github.com/orlp/slotmap/blob/master/RELEASES.md) - [Commits](https://github.com/orlp/slotmap/compare/v1.0.5...v1.0.6) --- updated-dependencies: - dependency-name: slotmap dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7d8108f6..b92347281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,9 +887,9 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "slotmap" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a952280edbecfb1d4bd3cf2dbc309dc6ab523e53487c438ae21a6df09fe84bc4" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" dependencies = [ "version_check", ] From 40223bbb45240f6171d15101b65a29d6b14f01f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 01:01:41 +0000 Subject: [PATCH 12/18] build(deps): bump arc-swap from 1.3.1 to 1.3.2 Bumps [arc-swap](https://github.com/vorner/arc-swap) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/vorner/arc-swap/releases) - [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md) - [Commits](https://github.com/vorner/arc-swap/compare/v1.3.1...v1.3.2) --- updated-dependencies: - dependency-name: arc-swap dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b92347281..f10f3bb90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" [[package]] name = "arc-swap" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a23efe54373080cf871532e2d01076be41c4c896d32ef63af1b2dded924b03" +checksum = "b5ab7d9e73059c86c36473f459b52adbd99c3554a4fec492caef460806006f00" [[package]] name = "autocfg" From daff9f5fd24949aa57f2dc951de9cf24c5bc9b11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 01:03:05 +0000 Subject: [PATCH 13/18] build(deps): bump serde_json from 1.0.66 to 1.0.67 Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.66 to 1.0.67. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.66...v1.0.67) --- updated-dependencies: - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f10f3bb90..e1b2b3d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,9 +811,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" dependencies = [ "itoa", "ryu", From dbfd054562ba97a73128c49fae0cea95deabec5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 01:03:01 +0000 Subject: [PATCH 14/18] build(deps): bump serde from 1.0.129 to 1.0.130 Bumps [serde](https://github.com/serde-rs/serde) from 1.0.129 to 1.0.130. - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.129...v1.0.130) --- updated-dependencies: - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1b2b3d3a..298ad15b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,18 +791,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.129" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.129" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", From e772808a5b0417e4d074eb9683d79376f83dae2d Mon Sep 17 00:00:00 2001 From: Omnikar Date: Tue, 31 Aug 2021 05:13:16 -0400 Subject: [PATCH 15/18] Shell commands (#547) * Implement shell interaction commands * Use slice instead of iterator for shell invocation * Default to `sh` instead of `$SHELL` for shell commands * Enforce trailing comma in `commands` macro * Use `|` register for shell commands * Move shell config to `editor` and use in command * Update shell command prompts * Remove clone of shell config * Change shell function names to match prompts * Log stderr contents upon external command error * Remove `unwrap` calls on potential common errors `shell` will no longer panic if: * The user-configured shell cannot be found * The shell command does not output UTF-8 * Remove redundant `pipe` parameter * Rename `ShellBehavior::None` to `Ignore` * Display error when shell command is used and `shell = []` * Document shell commands in `keymap.md` --- book/src/keymap.md | 10 +++ helix-term/src/commands.rs | 136 ++++++++++++++++++++++++++++++++++++- helix-term/src/keymap.rs | 5 ++ helix-view/src/editor.rs | 7 ++ 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/book/src/keymap.md b/book/src/keymap.md index 613788637..d85fb9360 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -122,6 +122,16 @@ in reverse, or searching via smartcase. | `[D` | Go to first diagnostic in document | `goto_first_diag` | | `]D` | Go to last diagnostic in document | `goto_last_diag` | +### Shell + +| Key | Description | Command | +| ------ | ----------- | ------- | +| `\|` | Pipe each selection through shell command, replacing with output | `shell_pipe` | +| `A-\|` | Pipe each selection into shell command, ignoring output | `shell_pipe_to` | +| `!` | Run shell command, inserting output before each selection | `shell_insert_output` | +| `A-!` | Run shell command, appending output after each selection | `shell_append_output` | +| `$` | Pipe each selection into shell command, removing if the command exits >0 | `shell_keep_pipe` | + ## Select / extend mode I'm still pondering whether to keep this mode or not. It changes movement diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d21bbe42d..6437bf521 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -131,7 +131,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 { @@ -302,7 +302,12 @@ impl Command { surround_delete, "Surround delete", select_textobject_around, "Select around object", select_textobject_inner, "Select inside object", - 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", ); } @@ -4292,6 +4297,133 @@ 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:", ShellBehavior::Replace); +} + +fn shell_pipe_to(cx: &mut Context) { + shell(cx, "pipe-to:", ShellBehavior::Ignore); +} + +fn shell_insert_output(cx: &mut Context) { + shell(cx, "insert-output:", ShellBehavior::Insert); +} + +fn shell_append_output(cx: &mut Context) { + shell(cx, "append-output:", ShellBehavior::Append); +} + +fn shell_keep_pipe(cx: &mut Context) { + shell(cx, "keep-pipe:", ShellBehavior::Filter); +} + +fn shell(cx: &mut Context, prompt: &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.to_owned(), + Some('|'), + |_input: &str| Vec::new(), + move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + let shell = &cx.editor.config.shell; + if event == PromptEvent::Validate { + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); + let mut error: Option<&str> = None; + let transaction = + Transaction::change_by_selection(doc.text(), selection, |range| { + let mut process; + match Command::new(&shell[0]) + .args(&shell[1..]) + .arg(input) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(p) => process = p, + Err(e) => { + log::error!("Failed to start shell: {}", e); + error = Some("Failed to start shell"); + return (0, 0, None); + } + } + 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() { + let stderr = output.stderr; + if !stderr.is_empty() { + log::error!( + "Shell error: {}", + String::from_utf8_lossy(&stderr) + ); + } + error = Some("Command failed"); + return (0, 0, None); + } + let stdout = output.stdout; + let tendril; + match Tendril::try_from_byte_slice(&stdout) { + Ok(t) => tendril = t, + Err(_) => { + error = Some("Process did not output valid UTF-8"); + return (0, 0, None); + } + } + 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()), + }; + (from, to, Some(tendril)) + } else { + // if the process exits successfully, keep the selection, otherwise delete it. + let keep = output.status.success(); + ( + range.from(), + if keep { range.from() } else { range.to() }, + None, + ) + } + }); + + if let Some(error) = error { + cx.editor.set_error(error.to_owned()); + } else if behavior != ShellBehavior::Ignore { + 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 492dc2929..f3e160b1a 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -508,6 +508,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-view/src/editor.rs b/helix-view/src/editor.rs index 18cb9106e..e5ff93adb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -33,6 +33,8 @@ pub struct Config { pub scroll_lines: isize, /// Mouse support. Defaults to true. pub mouse: bool, + /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise. + pub shell: Vec, /// Line number mode. pub line_number: LineNumber, /// Middle click paste support. Defaults to true @@ -55,6 +57,11 @@ impl Default for Config { scrolloff: 5, scroll_lines: 3, mouse: true, + shell: if cfg!(windows) { + vec!["cmd".to_owned(), "/C".to_owned()] + } else { + vec!["sh".to_owned(), "-c".to_owned()] + }, line_number: LineNumber::Absolute, middle_click_paste: true, } From 4a76ea8f8834a880d63246c8d43587693ac7cc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 18:17:22 +0900 Subject: [PATCH 16/18] shell: Move changes outside so we can properly handle errors --- helix-term/src/commands.rs | 123 ++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6437bf521..42495b7eb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4346,74 +4346,73 @@ fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) { if event == PromptEvent::Validate { let (view, doc) = current!(cx.editor); let selection = doc.selection(view.id); - let mut error: Option<&str> = None; - let transaction = - Transaction::change_by_selection(doc.text(), selection, |range| { - let mut process; - match Command::new(&shell[0]) - .args(&shell[1..]) - .arg(input) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - Ok(p) => process = p, - Err(e) => { - log::error!("Failed to start shell: {}", e); - error = Some("Failed to start shell"); - return (0, 0, None); - } - } - if pipe { - let stdin = process.stdin.as_mut().unwrap(); - let fragment = range.fragment(doc.text().slice(..)); - stdin.write_all(fragment.as_bytes()).unwrap(); + + 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(p) => process = p, + 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() { - let stderr = output.stderr; - if !stderr.is_empty() { - log::error!( - "Shell error: {}", - String::from_utf8_lossy(&stderr) - ); - } - error = Some("Command failed"); - return (0, 0, None); + let output = process.wait_with_output().unwrap(); + if behavior != ShellBehavior::Filter { + if !output.status.success() { + let stderr = output.stderr; + if !stderr.is_empty() { + log::error!("Shell error: {}", String::from_utf8_lossy(&stderr)); } - let stdout = output.stdout; - let tendril; - match Tendril::try_from_byte_slice(&stdout) { - Ok(t) => tendril = t, - Err(_) => { - error = Some("Process did not output valid UTF-8"); - return (0, 0, None); - } + cx.editor.set_error("Command failed".to_owned()); + return; + } + let stdout = output.stdout; + let tendril; + match Tendril::try_from_byte_slice(&stdout) { + Ok(t) => tendril = t, + 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()), - }; - (from, to, Some(tendril)) - } else { - // if the process exits successfully, keep the selection, otherwise delete it. - let keep = output.status.success(); - ( - range.from(), - if keep { range.from() } else { range.to() }, - None, - ) } - }); + 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, + )); + } + } + + let transaction = Transaction::change(doc.text(), changes.into_iter()); - if let Some(error) = error { - cx.editor.set_error(error.to_owned()); - } else if behavior != ShellBehavior::Ignore { + if behavior != ShellBehavior::Ignore { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } From 9b96bb5ac802189f567b101f7c8da4b370a2fdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 18:24:24 +0900 Subject: [PATCH 17/18] Refactor a bit further --- helix-term/src/commands.rs | 127 ++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 42495b7eb..e75d46dc3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4342,80 +4342,77 @@ fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) { 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; - if event == PromptEvent::Validate { - let (view, doc) = current!(cx.editor); - let selection = doc.selection(view.id); + 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(p) => process = p, - 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 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(); - let output = process.wait_with_output().unwrap(); - if behavior != ShellBehavior::Filter { - if !output.status.success() { - let stderr = output.stderr; - if !stderr.is_empty() { - log::error!("Shell error: {}", String::from_utf8_lossy(&stderr)); - } - cx.editor.set_error("Command failed".to_owned()); - return; + if behavior != ShellBehavior::Filter { + if !output.status.success() { + if !output.stderr.is_empty() { + log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); } - let stdout = output.stdout; - let tendril; - match Tendril::try_from_byte_slice(&stdout) { - Ok(t) => tendril = t, - 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, - )); + 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, + )); } + } - let transaction = Transaction::change(doc.text(), changes.into_iter()); + let transaction = Transaction::change(doc.text(), changes.into_iter()); - if behavior != ShellBehavior::Ignore { - doc.apply(&transaction, view.id); - doc.append_changes_to_history(view.id); - } + if behavior != ShellBehavior::Ignore { + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); } }, ); From a3bd80a6fa4f07b0fa581eae416376763ba94345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 31 Aug 2021 18:29:24 +0900 Subject: [PATCH 18/18] ui: prompt: Avoid allocating a prompt name if it's a static string --- helix-term/src/commands.rs | 39 ++++++++++++++++--------------------- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- helix-term/src/ui/prompt.rs | 4 ++-- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e75d46dc3..f479a7a09 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1079,7 +1079,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) { @@ -1091,7 +1091,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); @@ -1157,15 +1157,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)); } @@ -2210,7 +2206,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 @@ -3819,7 +3815,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) { @@ -4307,26 +4303,26 @@ enum ShellBehavior { } fn shell_pipe(cx: &mut Context) { - shell(cx, "pipe:", ShellBehavior::Replace); + shell(cx, "pipe:".into(), ShellBehavior::Replace); } fn shell_pipe_to(cx: &mut Context) { - shell(cx, "pipe-to:", ShellBehavior::Ignore); + shell(cx, "pipe-to:".into(), ShellBehavior::Ignore); } fn shell_insert_output(cx: &mut Context) { - shell(cx, "insert-output:", ShellBehavior::Insert); + shell(cx, "insert-output:".into(), ShellBehavior::Insert); } fn shell_append_output(cx: &mut Context) { - shell(cx, "append-output:", ShellBehavior::Append); + shell(cx, "append-output:".into(), ShellBehavior::Append); } fn shell_keep_pipe(cx: &mut Context) { - shell(cx, "keep-pipe:", ShellBehavior::Filter); + shell(cx, "keep-pipe:".into(), ShellBehavior::Filter); } -fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) { +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() { @@ -4338,7 +4334,7 @@ fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) { ShellBehavior::Insert | ShellBehavior::Append => false, }; let prompt = Prompt::new( - prompt.to_owned(), + prompt, Some('|'), |_input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { @@ -4408,9 +4404,8 @@ fn shell(cx: &mut Context, prompt: &str, behavior: ShellBehavior) { } } - let transaction = Transaction::change(doc.text(), changes.into_iter()); - 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); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index f3f8670e2..0a1e24b5b 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 ef2c434c4..06e424ea1 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -202,7 +202,7 @@ impl Picker { 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 7197adea8..1d512ad22 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, Cow<'static, str>); pub struct Prompt { - prompt: String, + prompt: Cow<'static, str>, pub line: String, cursor: usize, completion: Vec, @@ -55,7 +55,7 @@ pub enum Movement { impl Prompt { pub fn new( - prompt: String, + prompt: Cow<'static, str>, history_register: Option, mut completion_fn: impl FnMut(&str) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,