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:
+1
-1
@@ -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
@@ -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!(
|
||||||
|
|||||||
Reference in New Issue
Block a user