diff --git a/helix-core/src/doc_formatter.rs b/helix-core/src/doc_formatter.rs index 8ab4cdfda..4145ce7a9 100644 --- a/helix-core/src/doc_formatter.rs +++ b/helix-core/src/doc_formatter.rs @@ -24,7 +24,7 @@ use helix_stdx::rope::{RopeGraphemes, RopeSliceExt}; use crate::graphemes::{Grapheme, GraphemeStr}; use crate::syntax::Highlight; use crate::text_annotations::TextAnnotations; -use crate::{movement, Change, LineEnding, Position, Rope, RopeSlice, Tendril}; +use crate::{movement, Change, LineEnding, Position, RopeSlice, Tendril}; /// TODO make Highlight a u32 to reduce the size of this enum to a single word. #[derive(Debug, Clone, Copy)] @@ -434,57 +434,6 @@ impl<'t> DocumentFormatter<'t> { pub fn next_visual_pos(&self) -> Position { self.visual_pos } - - fn find_indent<'a>(&self, line: usize, doc: RopeSlice<'a>) -> RopeSlice<'a> { - let line_start = doc.line_to_char(line); - let mut indent_end = movement::skip_while(doc, line_start, |ch| matches!(ch, ' ' | '\t')) - .unwrap_or(line_start); - let slice = doc.slice(indent_end..); - if let Some(token) = self - .text_fmt - .continue_comments - .iter() - .filter(|token| slice.starts_with(token)) - .max_by_key(|x| x.len()) - { - indent_end += token.chars().count(); - } - let indent_end = movement::skip_while(doc, indent_end, |ch| matches!(ch, ' ' | '\t')) - .unwrap_or(indent_end); - return doc.slice(line_start..indent_end); - } - - /// consumes the iterator and hard-wraps the input where soft wraps would - /// have been applied. It probably only makes sense to call this method if - /// soft_wrap is true. - pub fn reflow(&mut self, doc: &Rope, line_ending: LineEnding) -> Vec { - let slice = doc.slice(..); - let mut current_line = self.visual_pos.row; - let mut changes = Vec::new(); - while let Some(grapheme) = self.next() { - if !grapheme.is_whitespace() && grapheme.visual_pos.row != current_line { - let indent = Tendril::from(format!( - "{}{}", - line_ending.as_str(), - self.find_indent(doc.char_to_line(grapheme.char_idx - 1), slice) - )); - let mut whitespace_start = grapheme.char_idx; - let mut whitespace_end = grapheme.char_idx; - while whitespace_start > 0 && slice.char(whitespace_start - 1) == ' ' { - whitespace_start -= 1; - } - while whitespace_end < slice.chars().len() && slice.char(whitespace_end) == ' ' { - whitespace_end += 1; - } - changes.push((whitespace_start, whitespace_end, Some(indent))); - current_line = grapheme.visual_pos.row; - } - if grapheme.raw == Grapheme::Newline { - current_line += 1; - } - } - changes - } } impl<'t> Iterator for DocumentFormatter<'t> { @@ -535,3 +484,91 @@ impl<'t> Iterator for DocumentFormatter<'t> { Some(grapheme) } } + +pub struct ReflowOpts<'a> { + pub width: usize, + pub line_ending: LineEnding, + pub comment_tokens: &'a [String], +} + +impl ReflowOpts<'_> { + fn find_indent<'a>(&self, line: usize, doc: RopeSlice<'a>) -> RopeSlice<'a> { + let line_start = doc.line_to_char(line); + let mut indent_end = movement::skip_while(doc, line_start, |ch| matches!(ch, ' ' | '\t')) + .unwrap_or(line_start); + let slice = doc.slice(indent_end..); + if let Some(token) = self + .comment_tokens + .iter() + .filter(|token| slice.starts_with(token)) + .max_by_key(|x| x.len()) + { + indent_end += token.chars().count(); + } + let indent_end = movement::skip_while(doc, indent_end, |ch| matches!(ch, ' ' | '\t')) + .unwrap_or(indent_end); + return doc.slice(line_start..indent_end); + } +} + +/// reflow wraps long lines in text to be less than opts.width. +pub fn reflow(text: RopeSlice, char_pos: usize, opts: &ReflowOpts) -> Vec { + // A constant so that reflow behaves consistently across + // different configurations. + const TAB_WIDTH: u16 = 8; + + let line_idx = text.char_to_line(char_pos.min(text.len_chars())); + let mut char_pos = text.line_to_char(line_idx); + + let mut col = 0; + let mut word_width = 0; + let mut last_word_boundary = None; + let mut changes = Vec::new(); + for grapheme in text.graphemes() { + let grapheme_chars = grapheme.len_chars(); + let mut grapheme = Grapheme::new(GraphemeStr::from(Cow::from(grapheme)), col, TAB_WIDTH); + if col + grapheme.width() > opts.width && !grapheme.is_whitespace() { + if let Some(n) = last_word_boundary { + let indent = opts.find_indent(text.char_to_line(n - 1), text); + let mut whitespace_start = n; + let mut whitespace_end = n; + while whitespace_start > 0 && text.char(whitespace_start - 1) == ' ' { + whitespace_start -= 1; + } + while whitespace_end < text.chars().len() && text.char(whitespace_end) == ' ' { + whitespace_end += 1; + } + changes.push(( + whitespace_start, + whitespace_end, + Some(Tendril::from(format!( + "{}{}", + opts.line_ending.as_str(), + &indent + ))), + )); + + col = 0; + for g in indent.graphemes() { + let g = Grapheme::new(GraphemeStr::from(Cow::from(g)), col, TAB_WIDTH); + col += g.width(); + } + col += word_width; + last_word_boundary = None; + grapheme.change_position(col, TAB_WIDTH); + } + } + col += grapheme.width(); + word_width += grapheme.width(); + if grapheme == Grapheme::Newline { + col = 0; + word_width = 0; + last_word_boundary = None; + } else if grapheme.is_whitespace() { + last_word_boundary = Some(char_pos); + word_width = 0; + } + char_pos += grapheme_chars; + } + changes +} diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1c8ee546e..f5c8e0798 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -7,7 +7,7 @@ use crate::job::Job; use super::*; use helix_core::command_line::{Args, Flag, Signature, Token, TokenKind}; -use helix_core::doc_formatter::DocumentFormatter; +use helix_core::doc_formatter::ReflowOpts; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::line_ending; @@ -2161,33 +2161,22 @@ fn reflow(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyho let rope = doc.text(); let slice = rope.slice(..); - let format = TextFormat { - soft_wrap: true, - tab_width: 8, - max_wrap: None, - max_indent_retain: u16::try_from(text_width).unwrap_or(u16::MAX), - wrap_indicator: Box::from(""), - wrap_indicator_highlight: None, - viewport_width: u16::try_from(text_width).unwrap_or(u16::MAX), - soft_wrap_at_text_width: true, - continue_comments: Vec::from( - doc.language_config() - .and_then(|config| config.comment_tokens.as_deref()) - .unwrap_or(&[]), - ), - is_word_boundary: |g| g.is_whitespace(), + let opts = ReflowOpts { + width: text_width, + line_ending: doc.line_ending, + comment_tokens: doc + .language_config() + .and_then(|config| config.comment_tokens.as_deref()) + .unwrap_or(&[]), }; - let annotations = TextAnnotations::default(); let mut changes = Vec::new(); for selection in doc.selection(view.id) { - let mut formatter = DocumentFormatter::new_at_prev_checkpoint( + changes.append(&mut helix_core::doc_formatter::reflow( slice.slice(..selection.to()), - &format, - &annotations, selection.from(), - ); - changes.append(&mut formatter.reflow(rope, doc.line_ending)); + &opts, + )); } let transaction = Transaction::change(rope, changes.into_iter()); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index c59873ef5..f1b4d23c0 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -840,9 +840,11 @@ bla]#", "// #[|This is a really long comment that we want to break onto multiple lines.]#", ":lang rust:reflow 13", "// #[|This is a -// really long -// comment that -// we want to +// really +// long +// comment +// that we +// want to // break onto // multiple // lines.]#", @@ -859,9 +861,8 @@ bla]#", \t// mollit anim id est laborum. |]#", ":lang go:reflow 50", - "#[\t// Lorem ipsum dolor sit amet, -\t// consectetur adipiscing elit, sed do -\t// eiusmod + "#[\t// Lorem ipsum dolor sit amet, consectetur +\t// adipiscing elit, sed do eiusmod \t// tempor incididunt ut labore et dolore \t// magna aliqua. Ut enim ad minim \t// veniam, quis nostrud exercitation @@ -886,8 +887,8 @@ bla]#", // that each need wrapping /// currently we wrap each line - /// completely separately in order to - /// preserve existing newlines.]#" + /// completely separately in order + /// to preserve existing newlines.]#" )) .await?;