diff --git a/docs/editing.md b/docs/editing.md index 4cece99..514ced4 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -236,7 +236,7 @@ Use `:word_wrap.toggle`, the **Toggle word wrap** palette action, or `config.cor Toggling preserves the source caret and anchors a visible caret to its previous screen row in ordinary and diff buffers. File boundaries clamp the anchor when there are not enough preceding rows. Horizontal position follows the reflowed text. -Text wraps at word boundaries where possible, with grapheme-safe breaks for long words. Whitespace is preserved; tabs keep their source-column stops and wide/combining Unicode characters retain their geometry. Only the first display segment shows a line number. Syntax and semantic colors, diagnostics, search matches, selections, and cursors follow the wrapped text. Motions and counts still use source lines, so `j`/`k`, Home/End, and saved file contents are unchanged. Vertical wheel input scrolls continuation rows; switching wrapping off restores horizontal scrolling. +Text wraps at word boundaries where possible, with grapheme-safe breaks for long words. Word-break opportunities require non-whitespace text on the visual row, so indentation alone does not push a long identifier onto a new row. Whitespace is preserved; tabs keep their source-column stops and wide/combining Unicode characters retain their geometry. Only the first display segment shows a line number. Syntax and semantic colors, diagnostics, search matches, selections, and cursors follow the wrapped text. Motions and counts still use source lines, so `j`/`k`, Home/End, and saved file contents are unchanged. Vertical wheel input scrolls continuation rows; switching wrapping off restores horizontal scrolling. ## Current-line Git blame diff --git a/src/wrap.rs b/src/wrap.rs index 7201e26..397b25a 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -36,6 +36,7 @@ impl WrapCache { let mut start = 0; let mut column = 0; let mut word_boundary = 0; + let mut content_end = 0; for grapheme in text.graphemes(true) { let cells = if grapheme == "\t" { TAB_WIDTH - column % TAB_WIDTH @@ -64,7 +65,10 @@ impl WrapCache { } } column += cells; - if grapheme.chars().all(char::is_whitespace) { + if !grapheme.chars().all(char::is_whitespace) { + content_end = column; + } else if content_end > start { + // A word break must not leave only whitespace on this visual row. word_boundary = column; } } @@ -116,6 +120,30 @@ mod tests { assert_eq!(caret_segment(&segments, 8), 2); } + #[test] + fn long_words_do_not_wrap_indentation_onto_an_empty_visual_row() { + for source in [ + " abcdefghijk", + "\tabcdefghijk", + "head abcdefghijk", + ] { + let expanded = source.replace('\t', " "); + let segments = layout(source, 8); + assert_eq!(segments.first().unwrap().start, 0); + assert_eq!(segments.last().unwrap().end, expanded.len()); + assert!(segments.windows(2).all(|pair| pair[0].end == pair[1].start)); + for segment in segments + .iter() + .filter(|segment| segment.start < segment.end) + { + assert!( + !expanded[segment.start..segment.end].trim().is_empty(), + "whitespace-only visual row {segment:?} in {source:?}" + ); + } + } + } + #[test] fn tabs_use_source_stops_and_unicode_graphemes_remain_intact() { assert_eq!(