From e0c0d598569159b217d3b58f05c49f7fcb46f07c Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Fri, 20 Sep 2024 21:15:17 -0700 Subject: [PATCH] Handle single-line comment prefixes in :reflow. This uses the single-line comment prefixes from the language config, so it should be able to handle different languages in a robust way. The logic is fairly simple, and doesn't handle block comments, for example. Fixes #3332, #3622 --- helix-core/src/wrap.rs | 169 ++++++++++++++++++++++++++++++- helix-term/src/commands/typed.rs | 8 +- 2 files changed, 173 insertions(+), 4 deletions(-) diff --git a/helix-core/src/wrap.rs b/helix-core/src/wrap.rs index f32d6f4bc11d..5bfbdda1da5a 100644 --- a/helix-core/src/wrap.rs +++ b/helix-core/src/wrap.rs @@ -1,9 +1,172 @@ use smartstring::{LazyCompact, SmartString}; +use std::fmt::Write; use textwrap::{Options, WordSplitter::NoHyphenation}; -/// Given a slice of text, return the text re-wrapped to fit it -/// within the given width. -pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString { +fn reflow_textwrap(text: &str, text_width: usize) -> String { let options = Options::new(text_width).word_splitter(NoHyphenation); textwrap::refill(text, options).into() } + +fn tab_len(str: &str) -> usize { + let mut n = 0; + for c in str.chars() { + if c == '\t' { + n += 8 - n % 8; + } else { + n += 1; + } + } + n +} + +fn trim_comment_prefix<'a, 'b>( + s: &'a str, + comment_prefixes: &'b [String], +) -> Option<(&'b str, &'a str)> { + for pfx in comment_prefixes { + match s.strip_prefix(pfx) { + Some(s) => return Some((pfx, s.trim_start())), + None => continue, + } + } + return None; +} + +/// Given a slice of text, return the text re-wrapped to fit it +/// within the given width. +pub fn reflow_hard_wrap( + text: &str, + text_width: usize, + comment_prefixes: &[String], +) -> SmartString { + // Strip indent and line comment prefix from each line. We can do a + // better job than textwrap since we know the comment prefix. + let first_line = match text.lines().next() { + Some(x) => x, + // text is empty for some reason. + None => return "".into(), + }; + let indent_end = first_line + .find(|c: char| !c.is_whitespace()) + .unwrap_or(first_line.len()); + let indent = &first_line[..indent_end]; + let (comment_prefix, _) = match trim_comment_prefix(&first_line[indent_end..], comment_prefixes) + { + Some(x) => x, + // not commented + None => return reflow_textwrap(text, text_width).into(), + }; + + let mut stripped_text = String::new(); + let mut sep = ""; + // I am intentionally using .split("\n") instead of .lines() here, + // so that if the text has \r\n line endings we don't destroy them. + for line in text.split("\n") { + let line = line.trim_start(); + let line = match trim_comment_prefix(line, comment_prefixes) { + Some((_, x)) => x, + None => line, + }; + write!(stripped_text, "{sep}{line}").unwrap(); + sep = "\n"; + } + println!("{:?}", stripped_text); + // Defer to textwrap for the actual wrapping, then copy back the + // indent and comment prefix from the first line onto all lines. + let wrapped_text = reflow_textwrap( + &stripped_text, + text_width + .saturating_sub(tab_len(indent)) + // unfortunately this doesn't correctly count multibyte + // characters in the comment prefix + .saturating_sub(comment_prefix.len()), + ); + let mut out = String::new(); + let mut sep = ""; + for line in wrapped_text.split("\n") { + out.push_str(sep); + if line.trim_start() != "" { + write!(out, "{indent}{comment_prefix} {line}").unwrap(); + } + sep = "\n"; + } + return out.into(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reflow_line() { + assert_eq!( + reflow_hard_wrap("This is a long line bla bla bla", 4, &[]), + "This +is a +long +line +bla +bla +bla" + ); + } + + #[test] + fn reflow_single_line_comment() { + assert_eq!( + reflow_hard_wrap( + "// As you can see, the code works really well because of the way that it is", + 10, + &vec!["//".into()] + ), + "// As you +// can see, +// the code +// works +// really +// well +// because +// of the +// way that +// it is" + ); + } + + #[test] + fn reflow_tabbed() { + // https://github.com/helix-editor/helix/issues/3622 + assert_eq!( + reflow_hard_wrap( + "\t// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +\t// tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +\t// veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +\t// commodo consequat. Duis aute irure dolor in reprehenderit in voluptate +\t// velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +\t// occaecat cupidatat non proident, sunt in culpa qui officia deserunt +\t// mollit anim id est laborum. +", + 100, + &vec!["//".into()] + ), + "\t// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt +\t// ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation +\t// ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in +\t// reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur +\t// sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id +\t// est laborum. +" + ); + } + + #[test] + fn reflow_multiple_comment_prefixes() { + assert_eq!( + reflow_hard_wrap( + "// how many different\n# ways to type comments\n-- do you need?", + 20, + &vec!["//".into(), "#".into(), "--".into()] + ), + "// how many different\n// ways to type\n// comments do you\n// need?", + ); + } +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 7ad0369fc1bd..abf54aeb1514 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2122,7 +2122,13 @@ fn reflow( 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, text_width); + let comment_prefixes = doc + .language_config() + .and_then(|config| config.comment_tokens.as_ref()) + .map(|pfxs| &pfxs[..]) + .unwrap_or(&[]); + let reflowed_text = + helix_core::wrap::reflow_hard_wrap(&fragment, text_width, comment_prefixes); (range.from(), range.to(), Some(reflowed_text)) });