1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-08-28 11:36:17 +03:00
* docs: integrate upstream PR #116

* feat: integrate upstream PR #113

* chore: add portable-atomic for viewport atomics (upstream PR #111)

* fix(examples): integrate upstream PR #100

* fix(word): integrate upstream PR #98

* fix(widget): handle wide unicode char widths in horizontal scroll (upstream PR #94)

* feat(highlight): integrate upstream PR #93

* chore(release): prepare v0.8.0 docs and CI-clean merged upstream updates

* docs(license): rename LICENSE.txt to LICENSE and fix header capitalization
Этот коммит содержится в:
srothgan
2026-02-18 14:34:50 +01:00
коммит произвёл GitHub
родитель b82ae1a9d6
Коммит 43afdc5ce1
11 изменённых файлов: 193 добавлений и 15 удалений

Просмотреть файл

@@ -1,3 +1,16 @@
<a id="v0.8.0"></a>
# [v0.8.0](https://github.com/srothgan/tui-textarea/releases/tag/v0.8.0) - 2026-02-18
- Add `TextArea::clear()` API to clear all text content with undo/redo coverage. (upstream PR [#113](https://github.com/rhysd/tui-textarea/pull/113))
- Add custom highlight ranges with priority via `TextArea::custom_highlight()` and `TextArea::clear_custom_highlight()`. (upstream PR [#93](https://github.com/rhysd/tui-textarea/pull/93))
- Fix horizontal scrolling with wide Unicode characters by using display width for cursor column calculations. (upstream PR [#94](https://github.com/rhysd/tui-textarea/pull/94))
- Treat `_` as part of words for word navigation/edit operations. (upstream PR [#98](https://github.com/rhysd/tui-textarea/pull/98))
- Fix editor examples to preserve modified state across multiple inputs. (upstream PR [#100](https://github.com/rhysd/tui-textarea/pull/100))
- Use `portable-atomic` for viewport atomics to improve target compatibility. (upstream PR [#111](https://github.com/rhysd/tui-textarea/pull/111))
- Fix README wording for line-number default behavior. (upstream PR [#116](https://github.com/rhysd/tui-textarea/pull/116))
[Changes][v0.8.0]
<a id="v0.7.1"></a> <a id="v0.7.1"></a>
# [v0.7.1](https://github.com/srothgan/tui-textarea/releases/tag/v0.7.1) - 2026-02-18 # [v0.7.1](https://github.com/srothgan/tui-textarea/releases/tag/v0.7.1) - 2026-02-18
@@ -449,6 +462,7 @@ First release :tada:
[Changes][v0.1.0] [Changes][v0.1.0]
[v0.8.0]: https://github.com/srothgan/tui-textarea/compare/v0.7.1...v0.8.0
[v0.7.1]: https://github.com/srothgan/tui-textarea/compare/v0.7.0...v0.7.1 [v0.7.1]: https://github.com/srothgan/tui-textarea/compare/v0.7.0...v0.7.1
[v0.7.0]: https://github.com/rhysd/tui-textarea/compare/v0.6.1...v0.7.0 [v0.7.0]: https://github.com/rhysd/tui-textarea/compare/v0.6.1...v0.7.0
[v0.6.1]: https://github.com/rhysd/tui-textarea/compare/v0.6.0...v0.6.1 [v0.6.1]: https://github.com/rhysd/tui-textarea/compare/v0.6.0...v0.6.1

Просмотреть файл

@@ -3,7 +3,7 @@ name = "tui-textarea-2"
homepage = "https://github.com/srothgan/tui-textarea#readme" homepage = "https://github.com/srothgan/tui-textarea#readme"
repository = "https://github.com/srothgan/tui-textarea" repository = "https://github.com/srothgan/tui-textarea"
documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/" documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/"
version = "0.7.1" version = "0.8.0"
edition = "2021" edition = "2021"
rust-version = "1.56.1" # for `tui` crate support rust-version = "1.56.1" # for `tui` crate support
authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"] authors = ["Simon Peter Rothgang <simonrothgang@icloud.com>", "rhysd <lin90162@yahoo.co.jp>"]
@@ -15,7 +15,7 @@ license = "MIT"
readme = "README.md" readme = "README.md"
categories = ["text-editors", "text-processing"] categories = ["text-editors", "text-processing"]
keywords = ["tui", "textarea", "editor", "input", "ratatui"] keywords = ["tui", "textarea", "editor", "input", "ratatui"]
include = ["/src", "/examples", "/tests", "/README.md", "/LICENSE.txt"] include = ["/src", "/examples", "/tests", "/README.md", "/LICENSE"]
[lib] [lib]
name = "tui_textarea" name = "tui_textarea"
@@ -46,6 +46,7 @@ crossterm-028 = { package = "crossterm", version = "0.28", optional = true }
crossterm-025 = { package = "crossterm", version = "0.25", optional = true } crossterm-025 = { package = "crossterm", version = "0.25", optional = true }
ratatui-core = { version = "0.1.0", default-features = false, optional = true } ratatui-core = { version = "0.1.0", default-features = false, optional = true }
ratatui-widgets = { version = "0.3.0", default-features = false, optional = true } ratatui-widgets = { version = "0.3.0", default-features = false, optional = true }
portable-atomic = "1.11.1"
regex = { version = "1", optional = true } regex = { version = "1", optional = true }
termion = { version = "4.0", optional = true } termion = { version = "4.0", optional = true }
termion-15 = { package = "termion", version = "1.5", optional = true } termion-15 = { package = "termion", version = "1.5", optional = true }

Просмотреть файл

@@ -1,4 +1,4 @@
the MIT License The MIT License
Copyright (c) 2022 rhysd Copyright (c) 2022 rhysd

Просмотреть файл

@@ -343,7 +343,7 @@ assert_eq!(textarea.into_lines(), [""]);
### Show line number ### Show line number
By default, `TextArea` does now show line numbers. To enable, set a style for rendering line numbers by By default, `TextArea` does not show line numbers. To enable, set a style for rendering line numbers by
`TextArea::set_line_number_style()`. For example, the following renders line numbers in dark gray background `TextArea::set_line_number_style()`. For example, the following renders line numbers in dark gray background
color. color.
@@ -485,6 +485,7 @@ notify how to move the cursor.
| `textarea.delete_line_by_head()` | Delete from cursor until the head of line | | `textarea.delete_line_by_head()` | Delete from cursor until the head of line |
| `textarea.delete_word()` | Delete one word before cursor | | `textarea.delete_word()` | Delete one word before cursor |
| `textarea.delete_next_word()` | Delete one word next to cursor | | `textarea.delete_next_word()` | Delete one word next to cursor |
| `textarea.clear()` | Clear all text |
| `textarea.undo()` | Undo | | `textarea.undo()` | Undo |
| `textarea.redo()` | Redo | | `textarea.redo()` | Redo |
| `textarea.copy()` | Copy selected text | | `textarea.copy()` | Copy selected text |
@@ -493,6 +494,8 @@ notify how to move the cursor.
| `textarea.start_selection()` | Start text selection | | `textarea.start_selection()` | Start text selection |
| `textarea.cancel_selection()` | Cancel text selection | | `textarea.cancel_selection()` | Cancel text selection |
| `textarea.select_all()` | Select entire text | | `textarea.select_all()` | Select entire text |
| `textarea.custom_highlight(range, style, priority)` | Add a custom highlighted range |
| `textarea.clear_custom_highlight()` | Clear all custom highlights |
| `textarea.move_cursor(CursorMove::Forward)` | Move cursor forward by one character | | `textarea.move_cursor(CursorMove::Forward)` | Move cursor forward by one character |
| `textarea.move_cursor(CursorMove::Back)` | Move cursor backward by one character | | `textarea.move_cursor(CursorMove::Back)` | Move cursor backward by one character |
| `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line | | `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line |
@@ -709,7 +712,7 @@ Please read [CONTRIBUTING.md](./CONTRIBUTING.md) before reporting an issue or ma
## License ## License
tui-textarea-2 is distributed under [The MIT License](./LICENSE.txt). tui-textarea-2 is distributed under [The MIT License](./LICENSE).
[crates-io-badge]: https://img.shields.io/crates/v/tui-textarea-2.svg [crates-io-badge]: https://img.shields.io/crates/v/tui-textarea-2.svg
[crate]: https://crates.io/crates/tui-textarea-2 [crate]: https://crates.io/crates/tui-textarea-2

Просмотреть файл

@@ -338,7 +338,7 @@ impl Editor<'_> {
} }
input => { input => {
let buffer = &mut self.buffers[self.current]; let buffer = &mut self.buffers[self.current];
buffer.modified = buffer.textarea.input(input); buffer.modified |= buffer.textarea.input(input);
} }
} }
} }

Просмотреть файл

@@ -341,7 +341,7 @@ impl Editor<'_> {
} }
input => { input => {
let buffer = &mut self.buffers[self.current]; let buffer = &mut self.buffers[self.current];
buffer.modified = buffer.textarea.input(input); buffer.modified |= buffer.textarea.input(input);
} }
} }
} }

Просмотреть файл

@@ -15,6 +15,7 @@ enum Boundary {
Select(Style), Select(Style),
#[cfg(feature = "search")] #[cfg(feature = "search")]
Search(Style), Search(Style),
Custom(Style, u8), // style, priority
End, End,
} }
@@ -22,10 +23,11 @@ impl Boundary {
fn cmp(&self, other: &Boundary) -> Ordering { fn cmp(&self, other: &Boundary) -> Ordering {
fn rank(b: &Boundary) -> u8 { fn rank(b: &Boundary) -> u8 {
match b { match b {
Boundary::Cursor(_) => 3, Boundary::Cursor(_) => 30,
#[cfg(feature = "search")] #[cfg(feature = "search")]
Boundary::Search(_) => 2, Boundary::Search(_) => 20,
Boundary::Select(_) => 1, Boundary::Select(_) => 10,
Boundary::Custom(_, p) => *p,
Boundary::End => 0, Boundary::End => 0,
} }
} }
@@ -38,6 +40,7 @@ impl Boundary {
Boundary::Select(s) => Some(*s), Boundary::Select(s) => Some(*s),
#[cfg(feature = "search")] #[cfg(feature = "search")]
Boundary::Search(s) => Some(*s), Boundary::Search(s) => Some(*s),
Boundary::Custom(s, _) => Some(*s),
Boundary::End => None, Boundary::End => None,
} }
} }
@@ -156,13 +159,15 @@ impl<'a> LineHighlighter<'a> {
} }
} }
pub fn selection( // Shared code for selection and custom highlights
fn multiline_highlight(
&mut self, &mut self,
current_row: usize, current_row: usize,
start_row: usize, start_row: usize,
start_off: usize, start_off: usize,
end_row: usize, end_row: usize,
end_off: usize, end_off: usize,
boundary: Boundary,
) { ) {
let (start, end) = if current_row == start_row { let (start, end) = if current_row == start_row {
if start_row == end_row { if start_row == end_row {
@@ -180,12 +185,47 @@ impl<'a> LineHighlighter<'a> {
return; return;
}; };
if start != end { if start != end {
self.boundaries self.boundaries.push((boundary, start));
.push((Boundary::Select(self.select_style), start));
self.boundaries.push((Boundary::End, end)); self.boundaries.push((Boundary::End, end));
} }
} }
pub fn selection(
&mut self,
current_row: usize,
start_row: usize,
start_off: usize,
end_row: usize,
end_off: usize,
) {
self.multiline_highlight(
current_row,
start_row,
start_off,
end_row,
end_off,
Boundary::Select(self.select_style),
);
}
pub fn custom(
&mut self,
current_row: usize,
range: ((usize, usize), (usize, usize)),
style: Style,
priority: u8,
) {
let ((start_row, start_off), (end_row, end_off)) = range;
self.multiline_highlight(
current_row,
start_row,
start_off,
end_row,
end_off,
Boundary::Custom(style, priority),
);
}
pub fn into_spans(self) -> Line<'a> { pub fn into_spans(self) -> Line<'a> {
let Self { let Self {
line, line,

Просмотреть файл

@@ -55,6 +55,13 @@ impl fmt::Display for YankText {
} }
} }
#[derive(Clone, Debug)]
struct CustomHighlight {
range: ((usize, usize), (usize, usize)),
style: Style,
priority: u8,
}
/// A type to manage state of textarea. These are some important methods: /// A type to manage state of textarea. These are some important methods:
/// ///
/// - [`TextArea::default`] creates an empty textarea. /// - [`TextArea::default`] creates an empty textarea.
@@ -125,6 +132,7 @@ pub struct TextArea<'a> {
mask: Option<char>, mask: Option<char>,
selection_start: Option<(usize, usize)>, selection_start: Option<(usize, usize)>,
select_style: Style, select_style: Style,
custom_highlights: Vec<CustomHighlight>,
} }
/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is /// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
@@ -230,6 +238,7 @@ impl<'a> TextArea<'a> {
mask: None, mask: None,
selection_start: None, selection_start: None,
select_style: Style::default().bg(Color::LightBlue), select_style: Style::default().bg(Color::LightBlue),
custom_highlights: Default::default(),
} }
} }
@@ -1299,6 +1308,37 @@ impl<'a> TextArea<'a> {
} }
} }
/// Clears the whole content of the text area.
/// This will result in an empty text area.
/// The position of the cursor does matter for clearing the entire text.
///
/// This method returns if some text was deleted or not in the textarea.
///
/// ```
/// use tui_textarea::TextArea;
///
/// let mut textarea = TextArea::from(["aaa\nbbb ccc"]);
///
/// textarea.clear();
/// assert!(textarea.is_empty());
/// ```
pub fn clear(&mut self) -> bool {
if self.is_empty() {
return false;
}
let all_lines = self.lines();
let summed_up_chars_new_lines: usize = all_lines
.iter()
.map(|line| line.chars().map(|_| 1usize).sum::<usize>())
.sum();
let all_chars = all_lines.len() + summed_up_chars_new_lines;
self.move_cursor(CursorMove::Jump(0, 0));
self.delete_str(all_chars);
true
}
/// Paste a string previously deleted by [`TextArea::delete_line_by_head`], [`TextArea::delete_line_by_end`], /// Paste a string previously deleted by [`TextArea::delete_line_by_head`], [`TextArea::delete_line_by_end`],
/// [`TextArea::delete_word`], [`TextArea::delete_next_word`]. This method returns if some text was inserted or not /// [`TextArea::delete_word`], [`TextArea::delete_next_word`]. This method returns if some text was inserted or not
/// in the textarea. /// in the textarea.
@@ -1355,6 +1395,23 @@ impl<'a> TextArea<'a> {
self.selection_start = None; self.selection_start = None;
} }
pub fn custom_highlight(
&mut self,
range: ((usize, usize), (usize, usize)),
style: Style,
priority: u8,
) {
self.custom_highlights.push(CustomHighlight {
range,
style,
priority,
});
}
pub fn clear_custom_highlight(&mut self) {
self.custom_highlights.clear();
}
/// Select the entire text. Cursor moves to the end of the text buffer. When text selection is already ongoing, /// Select the entire text. Cursor moves to the end of the text buffer. When text selection is already ongoing,
/// it is canceled. /// it is canceled.
/// ``` /// ```
@@ -1612,6 +1669,20 @@ impl<'a> TextArea<'a> {
hl.selection(row, start.row, start.offset, end.row, end.offset); hl.selection(row, start.row, start.offset, end.row, end.offset);
} }
for CustomHighlight {
range: ((start_row, start_offset), (end_row, end_offset)),
style,
priority,
} in &self.custom_highlights
{
hl.custom(
row,
((*start_row, *start_offset), (*end_row, *end_offset)),
*style,
*priority,
);
}
hl.into_spans() hl.into_spans()
} }

Просмотреть файл

@@ -4,12 +4,13 @@ use crate::ratatui::text::{Span, Text};
use crate::ratatui::widgets::{Paragraph, Widget}; use crate::ratatui::widgets::{Paragraph, Widget};
use crate::textarea::TextArea; use crate::textarea::TextArea;
use crate::util::num_digits; use crate::util::num_digits;
use portable_atomic::{AtomicU64, Ordering};
#[cfg(feature = "ratatui")] #[cfg(feature = "ratatui")]
use ratatui_core::text::Line; use ratatui_core::text::Line;
use std::cmp; use std::cmp;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "tuirs")] #[cfg(feature = "tuirs")]
use tui::text::Spans as Line; use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar;
// &mut 'a (u16, u16, u16, u16) is not available since `render` method takes immutable reference of TextArea // &mut 'a (u16, u16, u16, u16) is not available since `render` method takes immutable reference of TextArea
// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly // instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
@@ -114,7 +115,15 @@ impl<'a> TextArea<'a> {
} }
fn scroll_top_col(&self, prev_top: u16, width: u16) -> u16 { fn scroll_top_col(&self, prev_top: u16, width: u16) -> u16 {
let mut cursor = self.cursor().1 as u16; let (row, col) = self.cursor();
// Adujst the cursor position due to the width of non-latine characters.
let mut cursor = self.lines()[row]
.chars()
.take(col)
.map(|c| c.width().unwrap_or(0))
.sum::<usize>() as u16;
// Adjust the cursor position due to the width of line number. // Adjust the cursor position due to the width of line number.
if self.line_number_style().is_some() { if self.line_number_style().is_some() {
let lnum = num_digits(self.lines().len()) as u16 + 2; // `+ 2` for margins let lnum = num_digits(self.lines().len()) as u16 + 2; // `+ 2` for margins

Просмотреть файл

@@ -9,6 +9,8 @@ impl CharKind {
fn new(c: char) -> Self { fn new(c: char) -> Self {
if c.is_whitespace() { if c.is_whitespace() {
Self::Space Self::Space
} else if c == '_' {
Self::Other
} else if c.is_ascii_punctuation() { } else if c.is_ascii_punctuation() {
Self::Punct Self::Punct
} else { } else {

Просмотреть файл

@@ -1437,6 +1437,44 @@ fn test_delete_selection_before_insert() {
} }
} }
#[test]
fn test_clear_on_empty_text() {
let mut t = TextArea::default();
let was_cleared = t.clear();
assert!(!was_cleared);
}
#[test]
fn test_clear_text() {
let mut t = TextArea::default();
let input = "
Hello world
Again
";
t.insert_str(input);
let was_cleared = t.clear();
assert!(
t.lines().join("\n").is_empty(),
"Textarea should be empty after clearing the text"
);
assert!(was_cleared);
t.undo();
assert_eq!(
t.lines().join("\n"),
input,
"Textare should have the previous content before the reset. Orginal input: {input}",
);
t.redo();
assert!(
t.lines().join("\n").is_empty(),
"Textarea should be empty after clearing the text again"
);
}
#[test] #[test]
fn test_undo_redo_stop_selection() { fn test_undo_redo_stop_selection() {
fn check(t: &mut TextArea, f: fn(&mut TextArea) -> bool) { fn check(t: &mut TextArea, f: fn(&mut TextArea) -> bool) {