diff --git a/Cargo.lock b/Cargo.lock index 18f50af6..f075249a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,6 +374,7 @@ dependencies = [ "slotmap", "smallvec", "smartstring", + "textwrap", "toml", "tree-sitter", "unicode-general-category", @@ -1005,6 +1006,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "socket2" version = "0.4.4" @@ -1044,6 +1051,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -1179,6 +1197,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6" +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.19" diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index df0841b6..426598e3 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -59,6 +59,7 @@ | `:get-option`, `:get` | Get the current value of a config option. | | `:sort` | Sort ranges in selection. | | `:rsort` | Sort ranges in selection in reverse order. | +| `:reflow` | Hard-wrap the current selection of lines to a given width. | | `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:config-reload` | Refreshes helix's config. | | `:config-open` | Open the helix config.toml file. | diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 6e019a42..ab937f0b 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -41,6 +41,7 @@ encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } etcetera = "0.3" +textwrap = "0.15.0" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 02341265..a022a42a 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -27,6 +27,7 @@ pub mod syntax; pub mod test; pub mod textobject; mod transaction; +pub mod wrap; pub mod unicode { pub use unicode_general_category as category; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 3f9e7bcf..eab3ab79 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -67,6 +67,7 @@ pub struct LanguageConfiguration { pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, + pub max_line_length: Option, #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] pub config: Option, diff --git a/helix-core/src/wrap.rs b/helix-core/src/wrap.rs new file mode 100644 index 00000000..eabc47d4 --- /dev/null +++ b/helix-core/src/wrap.rs @@ -0,0 +1,7 @@ +use smartstring::{LazyCompact, SmartString}; + +/// Given a slice of text, return the text re-wrapped to fit it +/// within the given width. +pub fn reflow_hard_wrap(text: &str, max_line_len: usize) -> SmartString { + textwrap::refill(text, max_line_len).into() +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 373c7018..ec86e446 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1051,6 +1051,45 @@ fn sort_impl( Ok(()) } +fn reflow( + cx: &mut compositor::Context, + args: &[Cow], + _event: PromptEvent, +) -> anyhow::Result<()> { + let (view, doc) = current!(cx.editor); + + const DEFAULT_MAX_LEN: usize = 79; + + // Find the max line length by checking the following sources in order: + // - The passed argument in `args` + // - The configured max_line_len for this language in languages.toml + // - The const default we set above + let max_line_len: usize = args + .get(0) + .map(|num| num.parse::()) + .transpose()? + .or_else(|| { + doc.language_config() + .and_then(|config| config.max_line_length) + }) + .unwrap_or(DEFAULT_MAX_LEN); + + let rope = doc.text(); + + let selection = doc.selection(view.id); + let transaction = Transaction::change_by_selection(rope, selection, |range| { + let fragment = range.fragment(rope.slice(..)); + let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, max_line_len); + + (range.from(), range.to(), Some(reflowed_text)) + }); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + + Ok(()) +} + fn tree_sitter_subtree( cx: &mut compositor::Context, _args: &[Cow], @@ -1570,6 +1609,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: sort_reverse, completer: None, }, + TypableCommand { + name: "reflow", + aliases: &[], + doc: "Hard-wrap the current selection of lines to a given width.", + fun: reflow, + completer: None, + }, TypableCommand { name: "tree-sitter-subtree", aliases: &["ts-subtree"],