Skip to content

Commit

Permalink
Handle single-line comment prefixes in :reflow.
Browse files Browse the repository at this point in the history
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 helix-editor#3332, helix-editor#3622
  • Loading branch information
Rose Hogenson committed Sep 21, 2024
1 parent 9f93de5 commit fd96796
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 4 deletions.
168 changes: 165 additions & 3 deletions helix-core/src/wrap.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,171 @@
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<LazyCompact> {
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<LazyCompact> {
// 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";
}
let new_line_prefix = format!("{indent}{comment_prefix} ");
// 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
// unfortunately this doesn't correctly count multibyte
// characters in the comment prefix
.saturating_sub(tab_len(&new_line_prefix)),
);
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, "{new_line_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",
11,
&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\n// different ways to\n// type comments do\n// you need?",
);
}
}
8 changes: 7 additions & 1 deletion helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
});
Expand Down

0 comments on commit fd96796

Please sign in to comment.