From 00fb6d9ef39cf13046063629826c99d764d2a0d4 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Wed, 29 Mar 2023 13:19:15 -0700 Subject: [PATCH 01/11] add support to set text wrapping --- src/textarea.rs | 15 ++++++++++++++- src/widget.rs | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/textarea.rs b/src/textarea.rs index b8c77ca..07da1f9 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -11,7 +11,7 @@ use crate::word::{find_word_end_forward, find_word_start_backward}; use tui::layout::Alignment; use tui::style::{Modifier, Style}; use tui::text::Spans; -use tui::widgets::{Block, Widget}; +use tui::widgets::{Block, Widget, Wrap}; /// A type to manage state of textarea. /// @@ -37,6 +37,7 @@ use tui::widgets::{Block, Widget}; pub struct TextArea<'a> { lines: Vec, block: Option>, + wrap: Option, style: Style, cursor: (usize, usize), // 0-base tab_len: u8, @@ -134,6 +135,7 @@ impl<'a> TextArea<'a> { Self { lines, block: None, + wrap: None, style: Style::default(), cursor: (0, 0), tab_len: 4, @@ -1042,6 +1044,17 @@ impl<'a> TextArea<'a> { self.style } + /// Get current wrap setting of textarea. + pub fn get_wrap(&self) -> Option { + self.wrap + } + + /// Set text wrapping. By default, wrap is None. + /// Wrap type is from tui-rs crate, used for the Paragraph widget. + pub fn set_wrap(&mut self, wrap: Option) { + self.wrap = wrap + } + /// Set the block of textarea. By default, no block is set. /// ``` /// use tui_textarea::TextArea; diff --git a/src/widget.rs b/src/widget.rs index c71d71a..d2a3969 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -123,6 +123,9 @@ impl<'a> Widget for Renderer<'a> { let mut inner = Paragraph::new(text) .style(self.0.style()) .alignment(self.0.alignment()); + if let Some(wrap) = self.0.get_wrap() { + inner = inner.wrap(wrap); + } if let Some(b) = self.0.block() { inner = inner.block(b.clone()); } From 76cb94b8f9b1dd85234ec14a99d9a66afb68c6e0 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 21:12:52 -0700 Subject: [PATCH 02/11] wip: keep cursor on screen, silent crash --- src/textarea.rs | 13 +++-- src/widget.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/textarea.rs b/src/textarea.rs index 07da1f9..580119c 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -11,7 +11,7 @@ use crate::word::{find_word_end_forward, find_word_start_backward}; use tui::layout::Alignment; use tui::style::{Modifier, Style}; use tui::text::Spans; -use tui::widgets::{Block, Widget, Wrap}; +use tui::widgets::{Block, Widget}; /// A type to manage state of textarea. /// @@ -37,7 +37,7 @@ use tui::widgets::{Block, Widget, Wrap}; pub struct TextArea<'a> { lines: Vec, block: Option>, - wrap: Option, + wrap: bool, style: Style, cursor: (usize, usize), // 0-base tab_len: u8, @@ -135,7 +135,7 @@ impl<'a> TextArea<'a> { Self { lines, block: None, - wrap: None, + wrap: false, style: Style::default(), cursor: (0, 0), tab_len: 4, @@ -1045,13 +1045,12 @@ impl<'a> TextArea<'a> { } /// Get current wrap setting of textarea. - pub fn get_wrap(&self) -> Option { + pub fn get_wrap(&self) -> bool { self.wrap } - /// Set text wrapping. By default, wrap is None. - /// Wrap type is from tui-rs crate, used for the Paragraph widget. - pub fn set_wrap(&mut self, wrap: Option) { + /// Set text wrapping. By default, wrap is false. + pub fn set_wrap(&mut self, wrap: bool) { self.wrap = wrap } diff --git a/src/widget.rs b/src/widget.rs index d2a3969..abb30bb 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use tui::buffer::Buffer; use tui::layout::Rect; use tui::text::Text; -use tui::widgets::{Paragraph, Widget}; +use tui::widgets::{Paragraph, Widget, Wrap}; // &mut 'a (u16, u16, u16, u16) is not available since Renderer instance totally takes over the ownership of TextArea // instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly @@ -26,11 +26,13 @@ impl Clone for Viewport { } impl Viewport { + // Return coordinates at top of viewport pub fn scroll_top(&self) -> (u16, u16) { let u = self.0.load(Ordering::Relaxed); ((u >> 16) as u16, u as u16) } + // Return scroll top position, and width / height pub fn rect(&self) -> (u16, u16, u16, u16) { let u = self.0.load(Ordering::Relaxed); let width = (u >> 48) as u16; @@ -40,6 +42,7 @@ impl Viewport { (row, col, width, height) } + // What is the difference pub fn position(&self) -> (u16, u16, u16, u16) { let (row_top, col_top, width, height) = self.rect(); let row_bottom = row_top.saturating_add(height).saturating_sub(1); @@ -104,6 +107,20 @@ impl<'a> Widget for Renderer<'a> { area }; + // Transform lines into array of row count for each line + fn wrapped_rows(lines: &[String], wrap_width: u16) -> Vec { + lines + .iter() + .map(|line| { + let line_length = line.chars().count() as u16; + // Empty rows occupy at least 1 row + ((line_length + wrap_width - 1) / wrap_width).max(1) + }) + .collect() + } + + // Move scroll top if cursor moves outside of viewport + // Cursor position is relative to text lines, not viewport. fn next_scroll_top(prev_top: u16, cursor: u16, length: u16) -> u16 { if cursor < prev_top { cursor @@ -114,17 +131,113 @@ impl<'a> Widget for Renderer<'a> { } } + // Map row to wrap space, considering wrapped lines + fn to_wrap_space(row: u16, wrapped_rows: &Vec) -> u16 { + wrapped_rows + .iter() + .take(row as usize) + .map(|&row| row) + .sum::() + } + + // Map wrapped row index back to index that ignores wraps + fn to_line_space(wrap_row: u16, wrapped_rows: &Vec) -> u16 { + wrapped_rows + .iter() + .enumerate() + .scan(0, |acc, (i, &row)| { + // Add each line row count to acc + *acc += row; + // keep returning until wrap count exceeds row + if *acc <= wrap_row { + // return line index + Some(i as u16) + } else { + None + } + }) + // get last return before iteration passed the appropriate line + .last() + .unwrap_or(0) + } + + fn cursor_line_is_below_vp( + prev_top_row: u16, + cursor_row: u16, + viewport_height: u16, + wrapped_rows: &Vec, + ) -> bool { + if cursor_row < prev_top_row { + return false; + } + // Calculate the number of wrap rows between the top row and the cursor row + let rows_to_cursor = wrapped_rows[prev_top_row as usize..cursor_row as usize] + .iter() + .sum::(); + + // Check if the number of wrap rows between top and cursor exceeds the viewport height + rows_to_cursor > viewport_height + } + + fn next_scroll_row_wrapped( + prev_top_row: u16, + cursor_row: u16, + viewport_height: u16, + wrapped_rows: &Vec, + ) -> u16 { + if cursor_row < prev_top_row { + return cursor_row; + } else { + // Calculate the number of wrap rows between the top row and the cursor row + let rows_from_top_to_cursor = wrapped_rows + [prev_top_row as usize..cursor_row as usize] + .iter() + .sum::(); + let cursor_row_wraps = wrapped_rows[cursor_row as usize] - 1; + let cursor_line_on_screen = + rows_from_top_to_cursor + cursor_row_wraps <= viewport_height; + let rows_to_move = rows_from_top_to_cursor + cursor_row_wraps - viewport_height; + + if !cursor_line_on_screen { + // Count how many lines add up to enough rows to get entire cursor line on screen again + let lines_to_move = wrapped_rows[prev_top_row as usize..cursor_row as usize] + .iter() + .scan(0, |acc, &row| { + // Sum wrap rows to this line + *acc += row; + Some(*acc) + }) + // Return index of line where acc exceeds rows_to_move + .position(|sum| sum >= rows_to_move) + .unwrap_or(0) as u16; + + // Never move below cursor row in case terminal can't fit it + return (prev_top_row + lines_to_move).min(cursor_row); + } else { + return prev_top_row; + } + }; + } + let cursor = self.0.cursor(); - let (top_row, top_col) = self.0.viewport.scroll_top(); - let top_row = next_scroll_top(top_row, cursor.0 as u16, height); - let top_col = next_scroll_top(top_col, cursor.1 as u16, width); + let (mut top_row, mut top_col) = self.0.viewport.scroll_top(); + let wrap = self.0.get_wrap(); + if wrap { + let wrapped_rows = wrapped_rows(&self.0.lines(), width); + top_row = next_scroll_row_wrapped(top_row, cursor.0 as u16, height, &wrapped_rows); + // Column for scoll should never change with wrapping (no horiz scroll) + } else { + top_row = next_scroll_top(top_row, cursor.0 as u16, height); + top_col = next_scroll_top(top_col, cursor.1 as u16, width); + } + let (top_row, top_col) = (top_row, top_col); let text = self.text(top_row as usize, height as usize); let mut inner = Paragraph::new(text) .style(self.0.style()) .alignment(self.0.alignment()); - if let Some(wrap) = self.0.get_wrap() { - inner = inner.wrap(wrap); + if wrap { + inner = inner.wrap(Wrap { trim: false }); } if let Some(b) = self.0.block() { inner = inner.block(b.clone()); From 07523dd7429125cdbf30e7657523dd6eca9d9b89 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 21:33:34 -0700 Subject: [PATCH 03/11] chore: Remove unneeded helpers --- src/widget.rs | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index abb30bb..4be96da 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -131,54 +131,6 @@ impl<'a> Widget for Renderer<'a> { } } - // Map row to wrap space, considering wrapped lines - fn to_wrap_space(row: u16, wrapped_rows: &Vec) -> u16 { - wrapped_rows - .iter() - .take(row as usize) - .map(|&row| row) - .sum::() - } - - // Map wrapped row index back to index that ignores wraps - fn to_line_space(wrap_row: u16, wrapped_rows: &Vec) -> u16 { - wrapped_rows - .iter() - .enumerate() - .scan(0, |acc, (i, &row)| { - // Add each line row count to acc - *acc += row; - // keep returning until wrap count exceeds row - if *acc <= wrap_row { - // return line index - Some(i as u16) - } else { - None - } - }) - // get last return before iteration passed the appropriate line - .last() - .unwrap_or(0) - } - - fn cursor_line_is_below_vp( - prev_top_row: u16, - cursor_row: u16, - viewport_height: u16, - wrapped_rows: &Vec, - ) -> bool { - if cursor_row < prev_top_row { - return false; - } - // Calculate the number of wrap rows between the top row and the cursor row - let rows_to_cursor = wrapped_rows[prev_top_row as usize..cursor_row as usize] - .iter() - .sum::(); - - // Check if the number of wrap rows between top and cursor exceeds the viewport height - rows_to_cursor > viewport_height - } - fn next_scroll_row_wrapped( prev_top_row: u16, cursor_row: u16, From 86d8f24c0c65a74736941a31189f5efe38cab14e Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 21:33:51 -0700 Subject: [PATCH 04/11] fix: Overflowing subtraction --- src/widget.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widget.rs b/src/widget.rs index 4be96da..89e2110 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -148,7 +148,8 @@ impl<'a> Widget for Renderer<'a> { let cursor_row_wraps = wrapped_rows[cursor_row as usize] - 1; let cursor_line_on_screen = rows_from_top_to_cursor + cursor_row_wraps <= viewport_height; - let rows_to_move = rows_from_top_to_cursor + cursor_row_wraps - viewport_height; + let rows_to_move = + (rows_from_top_to_cursor + cursor_row_wraps).saturating_sub(viewport_height); if !cursor_line_on_screen { // Count how many lines add up to enough rows to get entire cursor line on screen again From ca67a01ac2a7e284db1634518ae25ab526d7f1f6 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 21:34:34 -0700 Subject: [PATCH 05/11] fix: Off-by-one errors. Scroll now always keeps cursor on screen --- src/widget.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widget.rs b/src/widget.rs index 89e2110..791cdc6 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -144,7 +144,8 @@ impl<'a> Widget for Renderer<'a> { let rows_from_top_to_cursor = wrapped_rows [prev_top_row as usize..cursor_row as usize] .iter() - .sum::(); + .sum::() + + 1; let cursor_row_wraps = wrapped_rows[cursor_row as usize] - 1; let cursor_line_on_screen = rows_from_top_to_cursor + cursor_row_wraps <= viewport_height; @@ -163,6 +164,7 @@ impl<'a> Widget for Renderer<'a> { // Return index of line where acc exceeds rows_to_move .position(|sum| sum >= rows_to_move) .unwrap_or(0) as u16; + let lines_to_move = lines_to_move + 1; // Convert from index // Never move below cursor row in case terminal can't fit it return (prev_top_row + lines_to_move).min(cursor_row); From 7bfa04d668e7d8a893ddbe5c01f996bc24564e88 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 21:35:53 -0700 Subject: [PATCH 06/11] chore: Remove some comments --- src/widget.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/widget.rs b/src/widget.rs index 791cdc6..144075a 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -26,13 +26,11 @@ impl Clone for Viewport { } impl Viewport { - // Return coordinates at top of viewport pub fn scroll_top(&self) -> (u16, u16) { let u = self.0.load(Ordering::Relaxed); ((u >> 16) as u16, u as u16) } - // Return scroll top position, and width / height pub fn rect(&self) -> (u16, u16, u16, u16) { let u = self.0.load(Ordering::Relaxed); let width = (u >> 48) as u16; @@ -42,7 +40,6 @@ impl Viewport { (row, col, width, height) } - // What is the difference pub fn position(&self) -> (u16, u16, u16, u16) { let (row_top, col_top, width, height) = self.rect(); let row_bottom = row_top.saturating_add(height).saturating_sub(1); From 8668bbf081f8bc67f996d886b2e326ff98d76e85 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sat, 8 Apr 2023 22:25:50 -0700 Subject: [PATCH 07/11] chore: Add comments --- src/widget.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/widget.rs b/src/widget.rs index 144075a..8673dca 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -111,6 +111,7 @@ impl<'a> Widget for Renderer<'a> { .map(|line| { let line_length = line.chars().count() as u16; // Empty rows occupy at least 1 row + // FIXME: This method doesn't work ((line_length + wrap_width - 1) / wrap_width).max(1) }) .collect() @@ -138,6 +139,7 @@ impl<'a> Widget for Renderer<'a> { return cursor_row; } else { // Calculate the number of wrap rows between the top row and the cursor row + // TODO: Clarify why +1 is needed let rows_from_top_to_cursor = wrapped_rows [prev_top_row as usize..cursor_row as usize] .iter() @@ -178,6 +180,7 @@ impl<'a> Widget for Renderer<'a> { let wrapped_rows = wrapped_rows(&self.0.lines(), width); top_row = next_scroll_row_wrapped(top_row, cursor.0 as u16, height, &wrapped_rows); // Column for scoll should never change with wrapping (no horiz scroll) + // FIXME: Edge case where line can't fit in screen and overflows? } else { top_row = next_scroll_top(top_row, cursor.0 as u16, height); top_col = next_scroll_top(top_col, cursor.1 as u16, width); @@ -197,6 +200,7 @@ impl<'a> Widget for Renderer<'a> { if top_col != 0 { inner = inner.scroll((0, top_col)); } + // TODO: Vertical scroll to position top edge in middle of wrapped line // Store scroll top position for rendering on the next tick self.0.viewport.store(top_row, top_col, width, height); From a51341795ff9b815dc42628bd655529a5119ca9d Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Fri, 21 Apr 2023 15:27:16 -0700 Subject: [PATCH 08/11] refactor: Extract fn to count rows in wrapped line, add tests --- src/util.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/widget.rs | 15 ++--- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/src/util.rs b/src/util.rs index f0e90e0..a1d5031 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,3 +6,154 @@ pub fn spaces(size: u8) -> &'static str { pub fn num_digits(i: usize) -> u8 { f64::log10(i as f64) as u8 + 1 } + +/// Calculate number of rows for a wrapped line +pub fn line_rows(line: &String, wrap_width: u16, has_lnum: bool, num_lines: usize) -> u16 { + let lnum_span_len = if has_lnum { + // Longest line number plus space on each side + num_digits(num_lines) + 2 + } else { + 0 + }; + + let words = line.split_whitespace(); + let mut current_line_len = lnum_span_len; + let mut wraps = 0; + + for word in words { + let word_len = word.chars().count() as u8; + + if (current_line_len + word_len + 1) as u16 > wrap_width { + wraps += 1; + current_line_len = 0; + } + + current_line_len += word_len + 1; + } + + // Add 1 to account for the last line + (wraps + 1).max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn run_line_rows_test( + line: &str, + wrap_width: u16, + has_lnum: bool, + num_lines: usize, + expected: u16, + ) { + let line = line.to_string(); + let result = line_rows(&line, wrap_width, has_lnum, num_lines); + assert_eq!( + result, expected, + "with string: '{}', width: {}, lnum: {}, num_lines: {}", + line, wrap_width, has_lnum, num_lines + ); + } + + #[test] + fn test_empty_line() { + run_line_rows_test("", 1, false, 1, 1); + run_line_rows_test("", 10, true, 10, 1); + } + + #[test] + fn test_no_wrapping() { + run_line_rows_test("Hello, world!", 13, false, 1, 1); + // _10_Hello, world! + run_line_rows_test("Hello, world!", 17, true, 10, 1); + } + + #[test] + fn test_wrapping() { + run_line_rows_test( + "A long line that should wrap at least once.", + 20, + false, + 1, + 3, + ); + run_line_rows_test( + "A long line that should wrap at least once.", + 20, + true, + 10, + 3, + ); + } + + #[test] + fn test_long_word_wrapping() { + // This line + // has a + // longwordth + // atwraps. + run_line_rows_test("This line has a longwordthatwraps.", 10, false, 1, 4); + // _10_This + // line has a + // longwordth + // atwraps. + run_line_rows_test("This line has a longwordthatwraps.", 10, true, 10, 4); + // _10_This + // line has a + // longwordth + // atwrapsmor + // e. + run_line_rows_test("This line has a longwordthatwrapsmore.", 10, true, 10, 5); + } + + #[test] + fn test_long_word_overflow() { + // This line + // has a + // longwordth + // atoverflow + // s. + run_line_rows_test("This line has a longwordthatoverflows.", 10, false, 1, 5); + // _100_ This + // line has a + // longwordth + // atoverflow + // s. + run_line_rows_test("This line has a longwordthatoverflows.", 10, true, 100, 5); + // _100000_ + // This line + // has a + // longwordth + // atoverflow + // s. + run_line_rows_test( + "This line has a longwordthatoverflows.", + 10, + true, + 100000, + 6, + ); + } + + #[test] + fn test_line_numbers() { + // _1_Word + // Word Word + // Word + run_line_rows_test("Word Word Word Word", 10, true, 1, 3); + // _1000_Word + // Word Word + // Word + run_line_rows_test("Word Word Word Word", 10, true, 1000, 3); + // _10000_ + // Word Word + // Word Word + run_line_rows_test("Word Word Word Word", 10, true, 10000, 3); + + // _1_ Longer + run_line_rows_test("Longer", 10, true, 1, 1); + // _10_ + // Longer + run_line_rows_test("Longer", 10, true, 10, 2); + } +} diff --git a/src/widget.rs b/src/widget.rs index 8673dca..766551c 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1,5 +1,5 @@ use crate::textarea::TextArea; -use crate::util::num_digits; +use crate::util::{line_rows, num_digits}; use std::cmp; use std::sync::atomic::{AtomicU64, Ordering}; use tui::buffer::Buffer; @@ -105,15 +105,11 @@ impl<'a> Widget for Renderer<'a> { }; // Transform lines into array of row count for each line - fn wrapped_rows(lines: &[String], wrap_width: u16) -> Vec { + fn wrapped_rows(lines: &[String], wrap_width: u16, has_lnum: bool) -> Vec { + let num_lines = lines.len(); lines .iter() - .map(|line| { - let line_length = line.chars().count() as u16; - // Empty rows occupy at least 1 row - // FIXME: This method doesn't work - ((line_length + wrap_width - 1) / wrap_width).max(1) - }) + .map(|line| line_rows(&line, wrap_width, has_lnum, num_lines)) .collect() } @@ -177,7 +173,8 @@ impl<'a> Widget for Renderer<'a> { let (mut top_row, mut top_col) = self.0.viewport.scroll_top(); let wrap = self.0.get_wrap(); if wrap { - let wrapped_rows = wrapped_rows(&self.0.lines(), width); + let wrapped_rows = + wrapped_rows(&self.0.lines(), width, self.0.line_number_style().is_some()); top_row = next_scroll_row_wrapped(top_row, cursor.0 as u16, height, &wrapped_rows); // Column for scoll should never change with wrapping (no horiz scroll) // FIXME: Edge case where line can't fit in screen and overflows? From db33e862561b43ba64e63b7587982a7cb5ff4f7c Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Fri, 21 Apr 2023 16:28:36 -0700 Subject: [PATCH 09/11] chore: Add comments --- src/util.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index a1d5031..4a3e986 100644 --- a/src/util.rs +++ b/src/util.rs @@ -20,14 +20,17 @@ pub fn line_rows(line: &String, wrap_width: u16, has_lnum: bool, num_lines: usiz let mut current_line_len = lnum_span_len; let mut wraps = 0; + // TODO: Consider length of tab characters for word in words { let word_len = word.chars().count() as u8; - if (current_line_len + word_len + 1) as u16 > wrap_width { + if (current_line_len + 1 + word_len) as u16 > wrap_width { wraps += 1; current_line_len = 0; } + // TODO count overflows + current_line_len += word_len + 1; } From 3fa765db968363ceefa05ee9417872e5d4dbd808 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sun, 23 Apr 2023 16:43:22 -0700 Subject: [PATCH 10/11] chore: Fixme for wrap counting fn --- src/util.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util.rs b/src/util.rs index 4a3e986..ad55111 100644 --- a/src/util.rs +++ b/src/util.rs @@ -30,6 +30,8 @@ pub fn line_rows(line: &String, wrap_width: u16, has_lnum: bool, num_lines: usiz } // TODO count overflows + // FIXME: Multiple whitespaces won't be counted between words + // FIXME: Tab characters will not be counted current_line_len += word_len + 1; } From f488234cf5958888def220bd39ef603461cf6822 Mon Sep 17 00:00:00 2001 From: Brooks Van Buren Date: Sun, 23 Apr 2023 17:37:54 -0700 Subject: [PATCH 11/11] feat: Rework algorithm for counting line wraps, count whitespaces and overflows --- src/util.rs | 72 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/util.rs b/src/util.rs index ad55111..a0dde21 100644 --- a/src/util.rs +++ b/src/util.rs @@ -16,32 +16,65 @@ pub fn line_rows(line: &String, wrap_width: u16, has_lnum: bool, num_lines: usiz 0 }; - let words = line.split_whitespace(); - let mut current_line_len = lnum_span_len; + let mut curr_line_len = lnum_span_len; let mut wraps = 0; + let mut in_whitespace = false; + let mut word_len = 0; + + // Return new cur_line_len and wraps resulting from word + fn add_word_to_line(word_len: u8, mut curr_line_len: u8, width: u8) -> (u8, u8) { + let mut wraps = 0; + + // Overflow case: Word cannot fit on a single line + // It is guaranteed to start on next line, and will wrap a known number of times + if word_len > width { + // Add one to round up, and one for initial wrap + wraps += (word_len / width) + 1 + 1; + curr_line_len = word_len % width; + return (curr_line_len, wraps); + } - // TODO: Consider length of tab characters - for word in words { - let word_len = word.chars().count() as u8; - - if (current_line_len + 1 + word_len) as u16 > wrap_width { + if curr_line_len + word_len > width { wraps += 1; - current_line_len = 0; + curr_line_len = word_len; + } else { + curr_line_len += word_len; } - // TODO count overflows - // FIXME: Multiple whitespaces won't be counted between words - // FIXME: Tab characters will not be counted + (curr_line_len, wraps) + } - current_line_len += word_len + 1; + for c in line.chars() { + if c.is_whitespace() { + // Add last complete word + if !in_whitespace && word_len > 0 { + let added_wraps; + (curr_line_len, added_wraps) = + add_word_to_line(word_len, curr_line_len, wrap_width as u8); + wraps += added_wraps; + word_len = 0; + in_whitespace = true; + } + if c == '\t' { + // FIXME: Count tabs properly + } + curr_line_len += 1; + } else { + if in_whitespace { + word_len = 0; + in_whitespace = false; + } + // FIXME: Unicode grapheme clusters are counted individually instead of visible char + word_len += 1; + } } // Add 1 to account for the last line - (wraps + 1).max(1) + (wraps + 1).max(1) as u16 } #[cfg(test)] -mod tests { +mod line_wrap_tests { use super::*; fn run_line_rows_test( @@ -161,4 +194,15 @@ mod tests { // Longer run_line_rows_test("Longer", 10, true, 10, 2); } + + #[test] + fn test_regression() { + run_line_rows_test( + "\"editor", + 58, + true, + 100, + 4, + ) + } }