зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-08 00:15:50 +03:00
consider wide chars on calculating tab width
Этот коммит содержится в:
@@ -43,6 +43,7 @@ tui = { version = "0.19", default-features = false, optional = true }
|
|||||||
arbitrary = { version = "1", features = ["derive"], optional = true }
|
arbitrary = { version = "1", features = ["derive"], optional = true }
|
||||||
crossterm-027 = { package = "crossterm", version = "0.27", optional = true }
|
crossterm-027 = { package = "crossterm", version = "0.27", optional = true }
|
||||||
ratatui = { version = "0.23.0", default-features = false, optional = true }
|
ratatui = { version = "0.23.0", default-features = false, optional = true }
|
||||||
|
unicode-width = "0.1.11"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "minimal"
|
name = "minimal"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use crate::tui::style::Style;
|
|||||||
feature = "ratatui-your-backend",
|
feature = "ratatui-your-backend",
|
||||||
))]
|
))]
|
||||||
use crate::tui::text::Line as Spans;
|
use crate::tui::text::Line as Spans;
|
||||||
|
|
||||||
use crate::tui::text::Span;
|
use crate::tui::text::Span;
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
feature = "ratatui-crossterm",
|
feature = "ratatui-crossterm",
|
||||||
@@ -13,11 +12,11 @@ use crate::tui::text::Span;
|
|||||||
feature = "ratatui-your-backend",
|
feature = "ratatui-your-backend",
|
||||||
)))]
|
)))]
|
||||||
use crate::tui::text::Spans;
|
use crate::tui::text::Spans;
|
||||||
|
|
||||||
use crate::util::{num_digits, spaces};
|
use crate::util::{num_digits, spaces};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
use unicode_width::UnicodeWidthChar as _;
|
||||||
|
|
||||||
enum Boundary {
|
enum Boundary {
|
||||||
Cursor(Style),
|
Cursor(Style),
|
||||||
@@ -58,7 +57,7 @@ fn line_display_text(s: &str, tab_len: u8, mask: Option<char>) -> Cow<'_, str> {
|
|||||||
|
|
||||||
let tab = spaces(tab_len);
|
let tab = spaces(tab_len);
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
let mut col = 0;
|
let mut width = 0;
|
||||||
for (i, c) in s.char_indices() {
|
for (i, c) in s.char_indices() {
|
||||||
if c == '\t' {
|
if c == '\t' {
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
@@ -66,15 +65,15 @@ fn line_display_text(s: &str, tab_len: u8, mask: Option<char>) -> Cow<'_, str> {
|
|||||||
buf.push_str(&s[..i]);
|
buf.push_str(&s[..i]);
|
||||||
}
|
}
|
||||||
if tab_len > 0 {
|
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]);
|
buf.push_str(&tab[..len]);
|
||||||
col += len;
|
width += len;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !buf.is_empty() {
|
if !buf.is_empty() {
|
||||||
buf.push(c);
|
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", 4, None), "ab ");
|
||||||
assert_eq!(&line_display_text("ab\t\t", 8, 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("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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use crate::tui::widgets::{Block, Widget};
|
|||||||
use crate::util::spaces;
|
use crate::util::spaces;
|
||||||
use crate::widget::{Renderer, Viewport};
|
use crate::widget::{Renderer, Viewport};
|
||||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||||
|
use unicode_width::UnicodeWidthChar as _;
|
||||||
|
|
||||||
/// A type to manage state of textarea.
|
/// 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
|
/// 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.
|
/// 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"]);
|
/// let mut textarea = TextArea::from(["hi"]);
|
||||||
///
|
///
|
||||||
|
/// textarea.move_cursor(CursorMove::End); // Move to the end of line
|
||||||
|
///
|
||||||
/// textarea.insert_tab();
|
/// 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 {
|
pub fn insert_tab(&mut self) -> bool {
|
||||||
if self.tab_len == 0 {
|
if self.tab_len == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let tab = if self.hard_tab_indent {
|
|
||||||
"\t"
|
if self.hard_tab_indent {
|
||||||
} else {
|
return self.insert_str("\t");
|
||||||
let len = self.tab_len - (self.cursor.1 % self.tab_len as usize) as u8;
|
}
|
||||||
spaces(len)
|
|
||||||
};
|
let (row, col) = self.cursor;
|
||||||
self.insert_str(tab)
|
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.
|
/// Insert a newline at current cursor position.
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
mod cursor;
|
mod cursor;
|
||||||
mod history;
|
mod history;
|
||||||
|
mod textarea;
|
||||||
|
|||||||
26
tests/textarea.rs
Обычный файл
26
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user