fix: avoid indentation-only wrapped rows

The shared wrapper treated leading whitespace as a usable word boundary,
pushing a long first token onto a continuation and leaving a numbered blank row.
Only record boundaries after non-whitespace content on the current visual row.
Preserve source whitespace, tab stops, caret rows and paired diff alignment.

Verified: 945 tests, formatting, and live ordinary/split-diff fixture smoke,
including wrap off/on and aligned closing brackets.
This commit is contained in:
Valdemar Lorenzen
2026-10-15 13:54:36 +02:00
parent 16490942d6
commit 75dc31b66f
2 changed files with 30 additions and 2 deletions
+1 -1
View File
@@ -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. 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 ## Current-line Git blame
+29 -1
View File
@@ -36,6 +36,7 @@ impl WrapCache {
let mut start = 0; let mut start = 0;
let mut column = 0; let mut column = 0;
let mut word_boundary = 0; let mut word_boundary = 0;
let mut content_end = 0;
for grapheme in text.graphemes(true) { for grapheme in text.graphemes(true) {
let cells = if grapheme == "\t" { let cells = if grapheme == "\t" {
TAB_WIDTH - column % TAB_WIDTH TAB_WIDTH - column % TAB_WIDTH
@@ -64,7 +65,10 @@ impl WrapCache {
} }
} }
column += cells; 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; word_boundary = column;
} }
} }
@@ -116,6 +120,30 @@ mod tests {
assert_eq!(caret_segment(&segments, 8), 2); 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] #[test]
fn tabs_use_source_stops_and_unicode_graphemes_remain_intact() { fn tabs_use_source_stops_and_unicode_graphemes_remain_intact() {
assert_eq!( assert_eq!(