diff --git a/CHANGELOG.md b/CHANGELOG.md
index be90e34..324ba6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+
+# [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]
+
# [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]
+[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.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
diff --git a/Cargo.toml b/Cargo.toml
index 284809e..e65c037 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ name = "tui-textarea-2"
homepage = "https://github.com/srothgan/tui-textarea#readme"
repository = "https://github.com/srothgan/tui-textarea"
documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/"
-version = "0.7.1"
+version = "0.8.0"
edition = "2021"
rust-version = "1.56.1" # for `tui` crate support
authors = ["Simon Peter Rothgang ", "rhysd "]
@@ -15,7 +15,7 @@ license = "MIT"
readme = "README.md"
categories = ["text-editors", "text-processing"]
keywords = ["tui", "textarea", "editor", "input", "ratatui"]
-include = ["/src", "/examples", "/tests", "/README.md", "/LICENSE.txt"]
+include = ["/src", "/examples", "/tests", "/README.md", "/LICENSE"]
[lib]
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 }
ratatui-core = { version = "0.1.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 }
termion = { version = "4.0", optional = true }
termion-15 = { package = "termion", version = "1.5", optional = true }
diff --git a/LICENSE.txt b/LICENSE
similarity index 98%
rename from LICENSE.txt
rename to LICENSE
index f4ea71e..dec136d 100644
--- a/LICENSE.txt
+++ b/LICENSE
@@ -1,4 +1,4 @@
-the MIT License
+The MIT License
Copyright (c) 2022 rhysd
diff --git a/README.md b/README.md
index afb0182..34417c8 100644
--- a/README.md
+++ b/README.md
@@ -343,7 +343,7 @@ assert_eq!(textarea.into_lines(), [""]);
### 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
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_word()` | Delete one word before cursor |
| `textarea.delete_next_word()` | Delete one word next to cursor |
+| `textarea.clear()` | Clear all text |
| `textarea.undo()` | Undo |
| `textarea.redo()` | Redo |
| `textarea.copy()` | Copy selected text |
@@ -493,6 +494,8 @@ notify how to move the cursor.
| `textarea.start_selection()` | Start text selection |
| `textarea.cancel_selection()` | Cancel text selection |
| `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::Back)` | Move cursor backward by one character |
| `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
-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
[crate]: https://crates.io/crates/tui-textarea-2
diff --git a/examples/editor.rs b/examples/editor.rs
index 309c5cf..c3f4c4c 100644
--- a/examples/editor.rs
+++ b/examples/editor.rs
@@ -338,7 +338,7 @@ impl Editor<'_> {
}
input => {
let buffer = &mut self.buffers[self.current];
- buffer.modified = buffer.textarea.input(input);
+ buffer.modified |= buffer.textarea.input(input);
}
}
}
diff --git a/examples/tuirs_editor.rs b/examples/tuirs_editor.rs
index dc1a6cd..792c2c0 100644
--- a/examples/tuirs_editor.rs
+++ b/examples/tuirs_editor.rs
@@ -341,7 +341,7 @@ impl Editor<'_> {
}
input => {
let buffer = &mut self.buffers[self.current];
- buffer.modified = buffer.textarea.input(input);
+ buffer.modified |= buffer.textarea.input(input);
}
}
}
diff --git a/src/highlight.rs b/src/highlight.rs
index 936859d..d7e15aa 100644
--- a/src/highlight.rs
+++ b/src/highlight.rs
@@ -15,6 +15,7 @@ enum Boundary {
Select(Style),
#[cfg(feature = "search")]
Search(Style),
+ Custom(Style, u8), // style, priority
End,
}
@@ -22,10 +23,11 @@ impl Boundary {
fn cmp(&self, other: &Boundary) -> Ordering {
fn rank(b: &Boundary) -> u8 {
match b {
- Boundary::Cursor(_) => 3,
+ Boundary::Cursor(_) => 30,
#[cfg(feature = "search")]
- Boundary::Search(_) => 2,
- Boundary::Select(_) => 1,
+ Boundary::Search(_) => 20,
+ Boundary::Select(_) => 10,
+ Boundary::Custom(_, p) => *p,
Boundary::End => 0,
}
}
@@ -38,6 +40,7 @@ impl Boundary {
Boundary::Select(s) => Some(*s),
#[cfg(feature = "search")]
Boundary::Search(s) => Some(*s),
+ Boundary::Custom(s, _) => Some(*s),
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,
current_row: usize,
start_row: usize,
start_off: usize,
end_row: usize,
end_off: usize,
+ boundary: Boundary,
) {
let (start, end) = if current_row == start_row {
if start_row == end_row {
@@ -180,12 +185,47 @@ impl<'a> LineHighlighter<'a> {
return;
};
if start != end {
- self.boundaries
- .push((Boundary::Select(self.select_style), start));
+ self.boundaries.push((boundary, start));
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> {
let Self {
line,
diff --git a/src/textarea.rs b/src/textarea.rs
index ad559f6..33c95fa 100644
--- a/src/textarea.rs
+++ b/src/textarea.rs
@@ -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:
///
/// - [`TextArea::default`] creates an empty textarea.
@@ -125,6 +132,7 @@ pub struct TextArea<'a> {
mask: Option,
selection_start: Option<(usize, usize)>,
select_style: Style,
+ custom_highlights: Vec,
}
/// 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,
selection_start: None,
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::())
+ .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`],
/// [`TextArea::delete_word`], [`TextArea::delete_next_word`]. This method returns if some text was inserted or not
/// in the textarea.
@@ -1355,6 +1395,23 @@ impl<'a> TextArea<'a> {
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,
/// it is canceled.
/// ```
@@ -1612,6 +1669,20 @@ impl<'a> TextArea<'a> {
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()
}
diff --git a/src/widget.rs b/src/widget.rs
index 463fa19..47e0450 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -4,12 +4,13 @@ use crate::ratatui::text::{Span, Text};
use crate::ratatui::widgets::{Paragraph, Widget};
use crate::textarea::TextArea;
use crate::util::num_digits;
+use portable_atomic::{AtomicU64, Ordering};
#[cfg(feature = "ratatui")]
use ratatui_core::text::Line;
use std::cmp;
-use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "tuirs")]
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
// 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 {
- 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::() as u16;
+
// Adjust the cursor position due to the width of line number.
if self.line_number_style().is_some() {
let lnum = num_digits(self.lines().len()) as u16 + 2; // `+ 2` for margins
diff --git a/src/word.rs b/src/word.rs
index f64f15c..e0d7b36 100644
--- a/src/word.rs
+++ b/src/word.rs
@@ -9,6 +9,8 @@ impl CharKind {
fn new(c: char) -> Self {
if c.is_whitespace() {
Self::Space
+ } else if c == '_' {
+ Self::Other
} else if c.is_ascii_punctuation() {
Self::Punct
} else {
diff --git a/tests/textarea.rs b/tests/textarea.rs
index 392f850..a7a5736 100644
--- a/tests/textarea.rs
+++ b/tests/textarea.rs
@@ -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]
fn test_undo_redo_stop_selection() {
fn check(t: &mut TextArea, f: fn(&mut TextArea) -> bool) {