diff --git a/Cargo.toml b/Cargo.toml index dad9c7e..b7607e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ tui = { version = "0.19", default-features = false, optional = true } arbitrary = { version = "1", features = ["derive"], optional = true } crossterm-027 = { package = "crossterm", version = "0.27", optional = true } ratatui = { version = "0.23.0", default-features = false, optional = true } +unicode-width = "0.1.11" [[example]] name = "minimal" diff --git a/src/highlight.rs b/src/highlight.rs index a452973..98ac75b 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -5,7 +5,6 @@ use crate::tui::style::Style; feature = "ratatui-your-backend", ))] use crate::tui::text::Line as Spans; - use crate::tui::text::Span; #[cfg(not(any( feature = "ratatui-crossterm", @@ -13,11 +12,11 @@ use crate::tui::text::Span; feature = "ratatui-your-backend", )))] use crate::tui::text::Spans; - use crate::util::{num_digits, spaces}; use std::borrow::Cow; use std::cmp::Ordering; use std::iter; +use unicode_width::UnicodeWidthChar as _; enum Boundary { Cursor(Style), @@ -58,7 +57,7 @@ fn line_display_text(s: &str, tab_len: u8, mask: Option) -> Cow<'_, str> { let tab = spaces(tab_len); let mut buf = String::new(); - let mut col = 0; + let mut width = 0; for (i, c) in s.char_indices() { if c == '\t' { if buf.is_empty() { @@ -66,15 +65,15 @@ fn line_display_text(s: &str, tab_len: u8, mask: Option) -> Cow<'_, str> { buf.push_str(&s[..i]); } if tab_len > 0 { - let len = tab_len as usize - (col % tab_len as usize); + let len = tab_len as usize - (width % tab_len as usize); buf.push_str(&tab[..len]); - col += len; + width += len; } } else { if !buf.is_empty() { buf.push(c); } - col += 1; + width += c.width().unwrap_or(0); } } @@ -245,5 +244,9 @@ mod tests { assert_eq!(&line_display_text("ab\t\t", 4, None), "ab "); assert_eq!(&line_display_text("ab\t\t", 8, None), "ab "); assert_eq!(&line_display_text("abcd\t", 4, None), "abcd "); + assert_eq!(&line_display_text( "あ\t", 0, None), "あ"); + assert_eq!(&line_display_text( "あ\t", 4, None), "あ "); + assert_eq!(&line_display_text( "🐶\t", 4, None), "🐶 "); + assert_eq!(&line_display_text( "あ\t", 4, Some('x')), "xx"); } } diff --git a/src/textarea.rs b/src/textarea.rs index eb65715..f689fc1 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -23,6 +23,7 @@ use crate::tui::widgets::{Block, Widget}; use crate::util::spaces; use crate::widget::{Renderer, Viewport}; use crate::word::{find_word_end_forward, find_word_start_backward}; +use unicode_width::UnicodeWidthChar as _; /// A type to manage state of textarea. /// @@ -686,26 +687,35 @@ impl<'a> TextArea<'a> { /// Insert a tab at current cursor position. Note that this method does nothing when the tab length is 0. This /// method returns if a tab string was inserted or not in the textarea. - /// textarea. /// ``` - /// use tui_textarea::TextArea; + /// use tui_textarea::{TextArea, CursorMove}; /// /// let mut textarea = TextArea::from(["hi"]); /// + /// textarea.move_cursor(CursorMove::End); // Move to the end of line + /// /// textarea.insert_tab(); - /// assert_eq!(textarea.lines(), [" hi"]); + /// assert_eq!(textarea.lines(), ["hi "]); + /// textarea.insert_tab(); + /// assert_eq!(textarea.lines(), ["hi "]); /// ``` pub fn insert_tab(&mut self) -> bool { if self.tab_len == 0 { return false; } - let tab = if self.hard_tab_indent { - "\t" - } else { - let len = self.tab_len - (self.cursor.1 % self.tab_len as usize) as u8; - spaces(len) - }; - self.insert_str(tab) + + if self.hard_tab_indent { + return self.insert_str("\t"); + } + + let (row, col) = self.cursor; + let width: usize = self.lines[row] + .chars() + .take(col) + .map(|c| c.width().unwrap_or(0)) + .sum(); + let len = self.tab_len - (width % self.tab_len as usize) as u8; + self.insert_str(spaces(len)) } /// Insert a newline at current cursor position. diff --git a/tests/mod.rs b/tests/mod.rs index d010018..b035d02 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,2 +1,3 @@ mod cursor; mod history; +mod textarea; diff --git a/tests/textarea.rs b/tests/textarea.rs new file mode 100644 index 0000000..e9adac6 --- /dev/null +++ b/tests/textarea.rs @@ -0,0 +1,26 @@ +use tui_textarea::{CursorMove, TextArea}; + +#[test] +fn test_insert_soft_tab() { + for test in [ + ("", 0, " "), + ("a", 1, "a "), + ("abcd", 4, "abcd "), + ("a", 0, " a"), + ("ab", 1, "a b"), + ("abcdefgh", 4, "abcd efgh"), + ("あ", 1, "あ "), + ("🐶", 1, "🐶 "), + ("あ", 0, " あ"), + ("あい", 1, "あ い"), + ] { + let (input, col, expected) = test; + let mut t = TextArea::from([input.to_string()]); + t.move_cursor(CursorMove::Jump(0, col)); + assert!(t.insert_tab(), "{:?}", test); + let lines = t.into_lines(); + assert_eq!(lines.len(), 1, "{:?}, {:?}", lines, test); + let line = &lines[0]; + assert_eq!(line, expected, "{:?}", test); + } +}