зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-07 16:05:50 +03:00
Merge branch 'search'
Этот коммит содержится в:
14
.github/workflows/ci.yml
поставляемый
14
.github/workflows/ci.yml
поставляемый
@@ -18,9 +18,9 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
- run: cargo test -- --skip src/lib.rs
|
- run: cargo test --all-features -- --skip src/lib.rs
|
||||||
if: ${{ matrix.os != 'windows-latest' }}
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
- run: cargo test -- --skip src\lib.rs
|
- run: cargo test --features=search -- --skip src\lib.rs
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -35,9 +35,7 @@ jobs:
|
|||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
override: true
|
override: true
|
||||||
- uses: Swatinem/rust-cache@v1
|
- uses: Swatinem/rust-cache@v1
|
||||||
- name: rustfmt
|
- run: cargo fmt -- --check
|
||||||
run: cargo fmt -- --check
|
- run: cargo clippy --examples --all-features -- -D warnings
|
||||||
- name: clippy
|
- run: cargo clippy --no-default-features -- -D warnings
|
||||||
run: cargo clippy --examples --all-features -- -D warnings
|
- run: cargo clippy --no-default-features --features termion -- -D warnings
|
||||||
- name: clippy with --no-default-features
|
|
||||||
run: cargo clippy --no-default-features -- -D warnings
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Please ensure that all tests and linter checks passed on your branch before crea
|
|||||||
To run tests:
|
To run tests:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test -- --skip src/lib.rs
|
cargo test --features=search -- --skip src/lib.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
`--skip` is necessary since `cargo test` tries to run code blocks in [README file](./README.md).
|
`--skip` is necessary since `cargo test` tries to run code blocks in [README file](./README.md).
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ include = [
|
|||||||
default = ["crossterm"]
|
default = ["crossterm"]
|
||||||
crossterm = ["dep:crossterm", "tui/crossterm"]
|
crossterm = ["dep:crossterm", "tui/crossterm"]
|
||||||
termion = ["dep:termion", "tui/termion"]
|
termion = ["dep:termion", "tui/termion"]
|
||||||
|
search = ["dep:regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = { version = "0.23", optional = true }
|
crossterm = { version = "0.23", optional = true }
|
||||||
|
regex = { version = "1", optional = true }
|
||||||
termion = { version = "1.5", optional = true }
|
termion = { version = "1.5", optional = true }
|
||||||
tui = { version = "0.18", default-features = false }
|
tui = { version = "0.18", default-features = false }
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ required-features = ["termion"]
|
|||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "editor"
|
name = "editor"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm", "search"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "split"
|
name = "split"
|
||||||
|
|||||||
130
README.md
130
README.md
@@ -14,6 +14,7 @@ text editor can be easily put as part of your TUI application.
|
|||||||
- Undo/Redo
|
- Undo/Redo
|
||||||
- Line number
|
- Line number
|
||||||
- Cursor line highlight
|
- Cursor line highlight
|
||||||
|
- Search with regular expressions
|
||||||
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
||||||
- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported
|
- Backend agnostic. [crossterm][], [termion][], and your own backend are all supported
|
||||||
- Multiple textarea widgets in the same screen
|
- Multiple textarea widgets in the same screen
|
||||||
@@ -24,40 +25,60 @@ text editor can be easily put as part of your TUI application.
|
|||||||
|
|
||||||
Running `cargo run --example` in this repository can demonstrate usage of tui-textarea.
|
Running `cargo run --example` in this repository can demonstrate usage of tui-textarea.
|
||||||
|
|
||||||
|
### [`minimal`](./examples/minimal.rs)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --example minimal
|
cargo run --example minimal
|
||||||
```
|
```
|
||||||
|
|
||||||
### [`minimal`](./examples/minimal.rs)
|
|
||||||
|
|
||||||
Minimal usage with [crossterm][] support.
|
Minimal usage with [crossterm][] support.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/minimal.gif" width=539 height=172 alt="minimal example">
|
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/minimal.gif" width=539 height=172 alt="minimal example">
|
||||||
|
|
||||||
### [`editor`](./examples/editor.rs)
|
### [`editor`](./examples/editor.rs)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --example editor --features search file.txt
|
||||||
|
```
|
||||||
|
|
||||||
Simple text editor to edit multiple files.
|
Simple text editor to edit multiple files.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/editor.gif" width=539 height=172 alt="editor example">
|
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/editor.gif" width=560 height=236 alt="editor example">
|
||||||
|
|
||||||
### [`single_line`](./examples/single_line.rs)
|
### [`single_line`](./examples/single_line.rs)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --example single_line
|
||||||
|
```
|
||||||
|
|
||||||
Single-line input form with float number validation.
|
Single-line input form with float number validation.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/single_line.gif" width=539 height=92 alt="single line example">
|
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/single_line.gif" width=539 height=92 alt="single line example">
|
||||||
|
|
||||||
### [`split`](./examples/split.rs)
|
### [`split`](./examples/split.rs)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --example split
|
||||||
|
```
|
||||||
|
|
||||||
Two split textareas in a screen and switch them. An example for multiple textarea instances.
|
Two split textareas in a screen and switch them. An example for multiple textarea instances.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/split.gif" width=539 height=124 alt="multiple textareas example">
|
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/split.gif" width=539 height=124 alt="multiple textareas example">
|
||||||
|
|
||||||
### [`termion`](./examples/termion.rs)
|
### [`termion`](./examples/termion.rs)
|
||||||
|
|
||||||
Minimal usage with [termion][] support. To run this example, `termion` feature needs to be enabled.
|
```sh
|
||||||
|
cargo run --example termion --features=termion
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal usage with [termion][] support.
|
||||||
|
|
||||||
### [`variable`](./examples/variable.rs)
|
### [`variable`](./examples/variable.rs)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --example variable
|
||||||
|
```
|
||||||
|
|
||||||
Simple textarea with variable height following the number of lines.
|
Simple textarea with variable height following the number of lines.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -70,6 +91,15 @@ tui = "*"
|
|||||||
tui-textarea = "*"
|
tui-textarea = "*"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you need text search with regular expressions, enable `search` feature. It adds [regex crate][regex] crate as
|
||||||
|
dependency.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
tui = "*"
|
||||||
|
tui-textarea = { version = "*", features = ["search"] }
|
||||||
|
```
|
||||||
|
|
||||||
If you're using tui-rs with [termion][], enable `termion` feature instead of `crossterm` feature.
|
If you're using tui-rs with [termion][], enable `termion` feature instead of `crossterm` feature.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -278,6 +308,44 @@ Setting 0 disables undo/redo.
|
|||||||
textarea.set_max_histories(0);
|
textarea.set_max_histories(0);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Text search with regular expressions
|
||||||
|
|
||||||
|
To search text in textarea, set a regular expression pattern with `TextArea::set_search_pattern()` and move cursor with
|
||||||
|
`TextArea::search_forward()` for forward search or `TextArea::search_back()` backward search. The regular expression is
|
||||||
|
handled by [`regex` crate][regex].
|
||||||
|
|
||||||
|
Text search wraps around the textarea. When searching forward and no match found until the end of textarea, it searches
|
||||||
|
the pattern from start of the file.
|
||||||
|
|
||||||
|
Matches are highlighted in textarea. The text style to highlight matches can be changed with
|
||||||
|
`TextArea::set_search_style()`. Setting an empty string to `TextArea::set_search_pattern()` stops the text search.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Start text search matching to "hello" or "hi". This highlights matches in textarea but does not move cursor.
|
||||||
|
// `regex::Error` is returned on invalid pattern.
|
||||||
|
textarea.set_search_pattern("(hello|hi)").unwrap();
|
||||||
|
|
||||||
|
textarea.search_forward(false); // Move cursor to the next match
|
||||||
|
textarea.search_back(false); // Move cursor to the previous match
|
||||||
|
|
||||||
|
// Setting empty string stops the search
|
||||||
|
textarea.set_search_pattern("").unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
No UI is provided for text search. You need to provide your own UI to input search query. It is recommended to use
|
||||||
|
another `TextArea` for search form. To build a single-line input form, see 'Single-line input like `<input>` in HTML' in
|
||||||
|
'Advanced Usage' section below.
|
||||||
|
|
||||||
|
[`editor` example](./examples/editor.rs) implements a text search with search form built on `TextArea`. See the
|
||||||
|
implementation for working example.
|
||||||
|
|
||||||
|
To use text search, `search` feature needs to be enabled in your `Cargo.toml`. It is disabled by default to avoid
|
||||||
|
depending on `regex` crate until it is necessary.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tui-textarea = { version = "*", features = ["search"] }
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Single-line input like `<input>` in HTML
|
### Single-line input like `<input>` in HTML
|
||||||
@@ -317,31 +385,34 @@ See [`single_line` example](./examples/single_line.rs) for working example.
|
|||||||
All editor operations are defined as public methods of `TextArea`. To move cursor, use `tui_textarea::CursorMove` to
|
All editor operations are defined as public methods of `TextArea`. To move cursor, use `tui_textarea::CursorMove` to
|
||||||
notify how to move the cursor.
|
notify how to move the cursor.
|
||||||
|
|
||||||
| Method | Operation |
|
| Method | Operation |
|
||||||
|------------------------------------------------------|-------------------------------------------|
|
|------------------------------------------------------|----------------------------------------------|
|
||||||
| `textarea.delete_char()` | Delete one character before cursor |
|
| `textarea.delete_char()` | Delete one character before cursor |
|
||||||
| `textarea.delete_next_char()` | Delete one character next to cursor |
|
| `textarea.delete_next_char()` | Delete one character next to cursor |
|
||||||
| `textarea.insert_newline()` | Insert newline |
|
| `textarea.insert_newline()` | Insert newline |
|
||||||
| `textarea.delete_line_by_end()` | Delete from cursor until the end of line |
|
| `textarea.delete_line_by_end()` | Delete from cursor until the end of line |
|
||||||
| `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.undo()` | Undo |
|
| `textarea.undo()` | Undo |
|
||||||
| `textarea.redo()` | Redo |
|
| `textarea.redo()` | Redo |
|
||||||
| `textarea.paste()` | Paste yanked text |
|
| `textarea.paste()` | Paste yanked text |
|
||||||
| `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 |
|
||||||
| `textarea.move_cursor(CursorMove::Down)` | Move cursor down by one line |
|
| `textarea.move_cursor(CursorMove::Down)` | Move cursor down by one line |
|
||||||
| `textarea.move_cursor(CursorMove::WordForward)` | Move cursor forward by word |
|
| `textarea.move_cursor(CursorMove::WordForward)` | Move cursor forward by word |
|
||||||
| `textarea.move_cursor(CursorMove::WordBack)` | Move cursor backward by word |
|
| `textarea.move_cursor(CursorMove::WordBack)` | Move cursor backward by word |
|
||||||
| `textarea.move_cursor(CursorMove::ParagraphForward)` | Move cursor up by paragraph |
|
| `textarea.move_cursor(CursorMove::ParagraphForward)` | Move cursor up by paragraph |
|
||||||
| `textarea.move_cursor(CursorMove::ParagraphBack)` | Move cursor down by paragraph |
|
| `textarea.move_cursor(CursorMove::ParagraphBack)` | Move cursor down by paragraph |
|
||||||
| `textarea.move_cursor(CursorMove::End)` | Move cursor to the end of line |
|
| `textarea.move_cursor(CursorMove::End)` | Move cursor to the end of line |
|
||||||
| `textarea.move_cursor(CursorMove::Head)` | Move cursor to the head of line |
|
| `textarea.move_cursor(CursorMove::Head)` | Move cursor to the head of line |
|
||||||
| `textarea.move_cursor(CursorMove::Top)` | Move cursor to top of lines |
|
| `textarea.move_cursor(CursorMove::Top)` | Move cursor to top of lines |
|
||||||
| `textarea.move_cursor(CursorMove::Bottom)` | Move cursor to bottom of lines |
|
| `textarea.move_cursor(CursorMove::Bottom)` | Move cursor to bottom of lines |
|
||||||
| `textarea.move_cursor(CursorMove::Jump(row, col))` | Move cursor to (row, col) position |
|
| `textarea.move_cursor(CursorMove::Jump(row, col))` | Move cursor to (row, col) position |
|
||||||
|
| `textarea.set_search_pattern(pattern)` | Set a pattern for text search |
|
||||||
|
| `textarea.search_forward(match_cursor)` | Move cursor to next match of text search |
|
||||||
|
| `textarea.search_back(match_cursor)` | Move cursor to previous match of text search |
|
||||||
|
|
||||||
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method. The
|
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method. The
|
||||||
following example defines modal key mappings like Vim.
|
following example defines modal key mappings like Vim.
|
||||||
@@ -549,3 +620,4 @@ tui-textarea is distributed under [The MIT License](./LICENSE.txt).
|
|||||||
[repo]: https://github.com/rhysd/tui-textarea
|
[repo]: https://github.com/rhysd/tui-textarea
|
||||||
[new-issue]: https://github.com/rhysd/tui-textarea/issues/new
|
[new-issue]: https://github.com/rhysd/tui-textarea/issues/new
|
||||||
[pulls]: https://github.com/rhysd/tui-textarea/pulls
|
[pulls]: https://github.com/rhysd/tui-textarea/pulls
|
||||||
|
[regex]: https://docs.rs/regex/latest/regex/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crossterm::terminal::{
|
|||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
@@ -13,9 +14,9 @@ use tui::backend::CrosstermBackend;
|
|||||||
use tui::layout::{Constraint, Direction, Layout};
|
use tui::layout::{Constraint, Direction, Layout};
|
||||||
use tui::style::{Color, Modifier, Style};
|
use tui::style::{Color, Modifier, Style};
|
||||||
use tui::text::{Span, Spans};
|
use tui::text::{Span, Spans};
|
||||||
use tui::widgets::Paragraph;
|
use tui::widgets::{Block, Borders, Paragraph};
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
use tui_textarea::{Input, Key, TextArea};
|
use tui_textarea::{CursorMove, Input, Key, TextArea};
|
||||||
|
|
||||||
macro_rules! error {
|
macro_rules! error {
|
||||||
($fmt: expr $(, $args:tt)*) => {{
|
($fmt: expr $(, $args:tt)*) => {{
|
||||||
@@ -23,6 +24,73 @@ macro_rules! error {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SearchBox<'a> {
|
||||||
|
textarea: TextArea<'a>,
|
||||||
|
open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for SearchBox<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut textarea = TextArea::default();
|
||||||
|
textarea.set_block(Block::default().borders(Borders::ALL).title("Search"));
|
||||||
|
Self {
|
||||||
|
textarea,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SearchBox<'a> {
|
||||||
|
fn open(&mut self) {
|
||||||
|
self.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.open = false;
|
||||||
|
// Remove input for next search. Do not recreate `self.textarea` instance to keep undo history so that users can
|
||||||
|
// restore previous input easily.
|
||||||
|
self.textarea.move_cursor(CursorMove::End);
|
||||||
|
self.textarea.delete_line_by_head();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> u16 {
|
||||||
|
if self.open {
|
||||||
|
3
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&mut self, input: Input) -> Option<&'_ str> {
|
||||||
|
match input {
|
||||||
|
Input {
|
||||||
|
key: Key::Enter, ..
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('m'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => None, // Disable shortcuts which inserts a newline. See `single_line` example
|
||||||
|
input => {
|
||||||
|
let modified = self.textarea.input(input);
|
||||||
|
modified.then(|| self.textarea.lines()[0].as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(&mut self, err: Option<impl Display>) {
|
||||||
|
let b = if let Some(err) = err {
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(format!("Search: {}", err))
|
||||||
|
.style(Style::default().fg(Color::Red))
|
||||||
|
} else {
|
||||||
|
Block::default().borders(Borders::ALL).title("Search")
|
||||||
|
};
|
||||||
|
self.textarea.set_block(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Buffer<'a> {
|
struct Buffer<'a> {
|
||||||
textarea: TextArea<'a>,
|
textarea: TextArea<'a>,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -73,6 +141,7 @@ struct Editor<'a> {
|
|||||||
buffers: Vec<Buffer<'a>>,
|
buffers: Vec<Buffer<'a>>,
|
||||||
term: Terminal<CrosstermBackend<io::Stdout>>,
|
term: Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
message: Option<Cow<'static, str>>,
|
message: Option<Cow<'static, str>>,
|
||||||
|
search: SearchBox<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Editor<'a> {
|
impl<'a> Editor<'a> {
|
||||||
@@ -99,28 +168,36 @@ impl<'a> Editor<'a> {
|
|||||||
buffers,
|
buffers,
|
||||||
term,
|
term,
|
||||||
message: None,
|
message: None,
|
||||||
|
search: SearchBox::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&mut self) -> io::Result<()> {
|
fn run(&mut self) -> io::Result<()> {
|
||||||
let layout = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Min(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
let search_height = self.search.height();
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(search_height),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
self.term.draw(|f| {
|
self.term.draw(|f| {
|
||||||
|
let chunks = layout.split(f.size());
|
||||||
|
|
||||||
|
if search_height > 0 {
|
||||||
|
f.render_widget(self.search.textarea.widget(), chunks[0]);
|
||||||
|
}
|
||||||
|
|
||||||
let buffer = &self.buffers[self.current];
|
let buffer = &self.buffers[self.current];
|
||||||
let textarea = &buffer.textarea;
|
let textarea = &buffer.textarea;
|
||||||
let chunks = layout.split(f.size());
|
|
||||||
let widget = textarea.widget();
|
let widget = textarea.widget();
|
||||||
f.render_widget(widget, chunks[0]);
|
f.render_widget(widget, chunks[1]);
|
||||||
|
|
||||||
// Render status line
|
// Render status line
|
||||||
let modified = if buffer.modified { " [modified]" } else { "" };
|
let modified = if buffer.modified { " [modified]" } else { "" };
|
||||||
@@ -138,7 +215,7 @@ impl<'a> Editor<'a> {
|
|||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(chunks[1]);
|
.split(chunks[2]);
|
||||||
let status_style = Style::default().add_modifier(Modifier::REVERSED);
|
let status_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||||
f.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]);
|
f.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]);
|
||||||
f.render_widget(Paragraph::new(path).style(status_style), status_chunks[1]);
|
f.render_widget(Paragraph::new(path).style(status_style), status_chunks[1]);
|
||||||
@@ -147,6 +224,24 @@ impl<'a> Editor<'a> {
|
|||||||
// Render message at bottom
|
// Render message at bottom
|
||||||
let message = if let Some(message) = self.message.take() {
|
let message = if let Some(message) = self.message.take() {
|
||||||
Spans::from(Span::raw(message))
|
Spans::from(Span::raw(message))
|
||||||
|
} else if search_height > 0 {
|
||||||
|
Spans::from(vec![
|
||||||
|
Span::raw("Press "),
|
||||||
|
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to jump to first match and close, "),
|
||||||
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to close, "),
|
||||||
|
Span::styled(
|
||||||
|
"^G or ↓ or ^N",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" to search next, "),
|
||||||
|
Span::styled(
|
||||||
|
"M-G or ↑ or ^P",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" to search previous"),
|
||||||
|
])
|
||||||
} else {
|
} else {
|
||||||
Spans::from(vec![
|
Spans::from(vec![
|
||||||
Span::raw("Press "),
|
Span::raw("Press "),
|
||||||
@@ -154,38 +249,98 @@ impl<'a> Editor<'a> {
|
|||||||
Span::raw(" to quit, "),
|
Span::raw(" to quit, "),
|
||||||
Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to save, "),
|
Span::raw(" to save, "),
|
||||||
|
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(" to search, "),
|
||||||
Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to switch buffer"),
|
Span::raw(" to switch buffer"),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
f.render_widget(Paragraph::new(message), chunks[2]);
|
f.render_widget(Paragraph::new(message), chunks[3]);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match Input::from(crossterm::event::read()?) {
|
if search_height > 0 {
|
||||||
Input {
|
let textarea = &mut self.buffers[self.current].textarea;
|
||||||
key: Key::Char('q'),
|
match Input::from(crossterm::event::read()?) {
|
||||||
ctrl: true,
|
Input {
|
||||||
..
|
key: Key::Char('g' | 'n'),
|
||||||
} => break,
|
ctrl: true,
|
||||||
Input {
|
alt: false,
|
||||||
key: Key::Char('x'),
|
}
|
||||||
ctrl: true,
|
| Input { key: Key::Down, .. } => {
|
||||||
..
|
if !textarea.search_forward(false) {
|
||||||
} => {
|
self.search.set_error(Some("Pattern not found"));
|
||||||
self.current = (self.current + 1) % self.buffers.len();
|
}
|
||||||
self.message = Some(format!("Switched to buffer #{}", self.current + 1).into());
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: false,
|
||||||
|
alt: true,
|
||||||
|
}
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('p'),
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
}
|
||||||
|
| Input { key: Key::Up, .. } => {
|
||||||
|
if !textarea.search_back(false) {
|
||||||
|
self.search.set_error(Some("Pattern not found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Enter, ..
|
||||||
|
} => {
|
||||||
|
if !textarea.search_forward(true) {
|
||||||
|
self.message = Some("Pattern not found".into());
|
||||||
|
}
|
||||||
|
self.search.close();
|
||||||
|
textarea.set_search_pattern("").unwrap();
|
||||||
|
}
|
||||||
|
Input { key: Key::Esc, .. } => {
|
||||||
|
self.search.close();
|
||||||
|
textarea.set_search_pattern("").unwrap();
|
||||||
|
}
|
||||||
|
input => {
|
||||||
|
if let Some(query) = self.search.input(input) {
|
||||||
|
let maybe_err = textarea.set_search_pattern(query).err();
|
||||||
|
self.search.set_error(maybe_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Input {
|
} else {
|
||||||
key: Key::Char('s'),
|
match Input::from(crossterm::event::read()?) {
|
||||||
ctrl: true,
|
Input {
|
||||||
..
|
key: Key::Char('q'),
|
||||||
} => {
|
ctrl: true,
|
||||||
self.buffers[self.current].save()?;
|
..
|
||||||
self.message = Some("Saved!".into());
|
} => break,
|
||||||
}
|
Input {
|
||||||
input => {
|
key: Key::Char('x'),
|
||||||
let buffer = &mut self.buffers[self.current];
|
ctrl: true,
|
||||||
buffer.modified = buffer.textarea.input(input);
|
..
|
||||||
|
} => {
|
||||||
|
self.current = (self.current + 1) % self.buffers.len();
|
||||||
|
self.message =
|
||||||
|
Some(format!("Switched to buffer #{}", self.current + 1).into());
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('s'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.buffers[self.current].save()?;
|
||||||
|
self.message = Some("Saved!".into());
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.search.open();
|
||||||
|
}
|
||||||
|
input => {
|
||||||
|
let buffer = &mut self.buffers[self.current];
|
||||||
|
buffer.modified = buffer.textarea.input(input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
431
src/textarea.rs
431
src/textarea.rs
@@ -2,7 +2,10 @@ use crate::cursor::CursorMove;
|
|||||||
use crate::history::{Edit, EditKind, History};
|
use crate::history::{Edit, EditKind, History};
|
||||||
use crate::input::{Input, Key};
|
use crate::input::{Input, Key};
|
||||||
use crate::word::{find_word_end_forward, find_word_start_backward};
|
use crate::word::{find_word_end_forward, find_word_start_backward};
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
use regex::Regex;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::cmp;
|
||||||
use std::sync::atomic::{AtomicU16, Ordering};
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
use tui::buffer::Buffer;
|
use tui::buffer::Buffer;
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
@@ -15,6 +18,41 @@ fn spaces(size: u8) -> &'static str {
|
|||||||
&SPACES[..size as usize]
|
&SPACES[..size as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn num_digits(i: usize) -> u8 {
|
||||||
|
f64::log10(i as f64) as u8 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight boundary in line
|
||||||
|
enum Boundary {
|
||||||
|
Cursor(Style),
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
Search(Style),
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Boundary {
|
||||||
|
fn cmp(&self, other: &Boundary) -> cmp::Ordering {
|
||||||
|
fn rank(b: &Boundary) -> u8 {
|
||||||
|
match b {
|
||||||
|
Boundary::Cursor(_) => 2,
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
Boundary::Search(_) => 1,
|
||||||
|
Boundary::End => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rank(self).cmp(&rank(other))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> Option<Style> {
|
||||||
|
match self {
|
||||||
|
Boundary::Cursor(s) => Some(*s),
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
Boundary::Search(s) => Some(*s),
|
||||||
|
Boundary::End => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A type to manage state of textarea.
|
/// A type to manage state of textarea.
|
||||||
///
|
///
|
||||||
/// [`TextArea::default`] creates an empty textarea. [`TextArea::new`] creates a textarea with given text lines.
|
/// [`TextArea::default`] creates an empty textarea. [`TextArea::new`] creates a textarea with given text lines.
|
||||||
@@ -48,6 +86,10 @@ pub struct TextArea<'a> {
|
|||||||
scroll_top: (AtomicU16, AtomicU16),
|
scroll_top: (AtomicU16, AtomicU16),
|
||||||
cursor_style: Style,
|
cursor_style: Style,
|
||||||
yank: String,
|
yank: String,
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
search_pat: Option<Regex>,
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
search_style: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -142,6 +184,10 @@ impl<'a> TextArea<'a> {
|
|||||||
scroll_top: (AtomicU16::new(0), AtomicU16::new(0)),
|
scroll_top: (AtomicU16::new(0), AtomicU16::new(0)),
|
||||||
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
|
||||||
yank: String::new(),
|
yank: String::new(),
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
search_pat: None,
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
search_style: Style::default().bg(tui::style::Color::Blue),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,6 +982,82 @@ impl<'a> TextArea<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Vec<Span<'b>> {
|
||||||
|
let mut spans = vec![];
|
||||||
|
|
||||||
|
if let Some(style) = self.line_number_style {
|
||||||
|
let pad = spaces(lnum_len - num_digits(row + 1) + 1);
|
||||||
|
spans.push(Span::styled(format!("{}{} ", pad, row + 1), style));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut boundaries = vec![]; // TODO: Consider smallvec
|
||||||
|
let mut cursor_at_end = false;
|
||||||
|
|
||||||
|
let style_begin = if row == self.cursor.0 {
|
||||||
|
if let Some((start, c)) = line.char_indices().nth(self.cursor.1) {
|
||||||
|
boundaries.push((Boundary::Cursor(self.cursor_style), start));
|
||||||
|
boundaries.push((Boundary::End, start + c.len_utf8()));
|
||||||
|
} else {
|
||||||
|
cursor_at_end = true;
|
||||||
|
}
|
||||||
|
self.cursor_line_style
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
if let Some(pat) = &self.search_pat {
|
||||||
|
for m in pat.find_iter(line) {
|
||||||
|
if m.start() != m.end() {
|
||||||
|
boundaries.push((Boundary::Search(self.search_style), m.start()));
|
||||||
|
boundaries.push((Boundary::End, m.end()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if boundaries.is_empty() {
|
||||||
|
spans.push(Span::raw(self.replace_tabs(line)));
|
||||||
|
if cursor_at_end {
|
||||||
|
spans.push(Span::styled(" ", self.cursor_style));
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
boundaries.sort_unstable_by(|(l, i), (r, j)| match i.cmp(j) {
|
||||||
|
cmp::Ordering::Equal => l.cmp(r),
|
||||||
|
o => o,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut boundaries = boundaries.into_iter();
|
||||||
|
let mut style = style_begin;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut stack = vec![];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some((next_boundary, end)) = boundaries.next() {
|
||||||
|
if start < end {
|
||||||
|
spans.push(Span::styled(self.replace_tabs(&line[start..end]), style));
|
||||||
|
}
|
||||||
|
|
||||||
|
style = if let Some(s) = next_boundary.style() {
|
||||||
|
stack.push(style);
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
stack.pop().unwrap_or(style_begin)
|
||||||
|
};
|
||||||
|
start = end;
|
||||||
|
} else {
|
||||||
|
if start != line.len() {
|
||||||
|
spans.push(Span::styled(self.replace_tabs(&line[start..]), style));
|
||||||
|
}
|
||||||
|
if cursor_at_end {
|
||||||
|
spans.push(Span::styled(" ", self.cursor_style));
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a tui-rs widget to render the current state of the textarea. The widget instance returned from this
|
/// Build a tui-rs widget to render the current state of the textarea. The widget instance returned from this
|
||||||
/// method can be rendered with [`tui::terminal::Frame::render_widget`].
|
/// method can be rendered with [`tui::terminal::Frame::render_widget`].
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
@@ -963,39 +1085,10 @@ impl<'a> TextArea<'a> {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn widget(&'a self) -> impl Widget + 'a {
|
pub fn widget(&'a self) -> impl Widget + 'a {
|
||||||
fn num_digits(i: usize) -> u8 {
|
|
||||||
f64::log10(i as f64) as u8 + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut lines = Vec::with_capacity(self.lines.len());
|
let mut lines = Vec::with_capacity(self.lines.len());
|
||||||
let line_number_len = num_digits(self.lines.len());
|
let lnum_len = num_digits(self.lines.len());
|
||||||
for (i, l) in self.lines.iter().map(String::as_str).enumerate() {
|
for (i, l) in self.lines.iter().map(String::as_str).enumerate() {
|
||||||
let mut spans = vec![];
|
let spans = self.line_spans(l, i, lnum_len);
|
||||||
|
|
||||||
if let Some(style) = self.line_number_style {
|
|
||||||
let pad = spaces(line_number_len - num_digits(i + 1) + 1);
|
|
||||||
spans.push(Span::styled(format!("{}{} ", pad, i + 1), style));
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == self.cursor.0 {
|
|
||||||
if let Some((i, c)) = l.char_indices().nth(self.cursor.1) {
|
|
||||||
let j = i + c.len_utf8();
|
|
||||||
spans.extend_from_slice(&[
|
|
||||||
Span::styled(self.replace_tabs(&l[..i]), self.cursor_line_style),
|
|
||||||
Span::styled(self.replace_tabs(&l[i..j]), self.cursor_style),
|
|
||||||
Span::styled(self.replace_tabs(&l[j..]), self.cursor_line_style),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
// When cursor is at the end of line
|
|
||||||
spans.extend_from_slice(&[
|
|
||||||
Span::styled(self.replace_tabs(l), self.cursor_line_style),
|
|
||||||
Span::styled(" ", self.cursor_style),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
spans.push(Span::raw(self.replace_tabs(l)));
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(Spans::from(spans));
|
lines.push(Spans::from(spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,6 +1419,282 @@ impl<'a> TextArea<'a> {
|
|||||||
pub fn set_yank_text(&mut self, text: impl Into<String>) {
|
pub fn set_yank_text(&mut self, text: impl Into<String>) {
|
||||||
self.yank = text.into();
|
self.yank = text.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a regular expression pattern for text search. Setting an empty string stops the text search.
|
||||||
|
/// When a valid pattern is set, all matches will be highlighted in the textarea. Note that the cursor does not
|
||||||
|
/// move. To move the cursor, use [`TextArea::search_forward`] and [`TextArea::search_back`].
|
||||||
|
///
|
||||||
|
/// Grammar of regular expression follows [regex crate](https://docs.rs/regex/latest/regex). Patterns don't match
|
||||||
|
/// to newlines so match passes across no newline.
|
||||||
|
///
|
||||||
|
/// When the pattern is invalid, the search pattern will not be updated and an error will be returned.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::from(["hello, world", "goodbye, world"]);
|
||||||
|
///
|
||||||
|
/// // Search "world"
|
||||||
|
/// textarea.set_search_pattern("world").unwrap();
|
||||||
|
///
|
||||||
|
/// assert_eq!(textarea.cursor(), (0, 0));
|
||||||
|
/// textarea.search_forward(false);
|
||||||
|
/// assert_eq!(textarea.cursor(), (0, 7));
|
||||||
|
/// textarea.search_forward(false);
|
||||||
|
/// assert_eq!(textarea.cursor(), (1, 9));
|
||||||
|
///
|
||||||
|
/// // Stop the text search
|
||||||
|
/// textarea.set_search_pattern("");
|
||||||
|
///
|
||||||
|
/// // Invalid search pattern
|
||||||
|
/// assert!(textarea.set_search_pattern("(hello").is_err());
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn set_search_pattern(&mut self, query: impl AsRef<str>) -> Result<(), regex::Error> {
|
||||||
|
let query = query.as_ref();
|
||||||
|
match &self.search_pat {
|
||||||
|
Some(r) if r.as_str() == query => {}
|
||||||
|
_ if query.is_empty() => self.search_pat = None,
|
||||||
|
_ => self.search_pat = Some(Regex::new(query)?),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a regular expression which was set by [`TextArea::set_search_pattern`]. When no text search is ongoing, this
|
||||||
|
/// method returns `None`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::default();
|
||||||
|
///
|
||||||
|
/// assert!(textarea.search_pattern().is_none());
|
||||||
|
/// textarea.set_search_pattern("hello+").unwrap();
|
||||||
|
/// assert!(textarea.search_pattern().is_some());
|
||||||
|
/// assert_eq!(textarea.search_pattern().unwrap().as_str(), "hello+");
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn search_pattern(&self) -> Option<&Regex> {
|
||||||
|
self.search_pat.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search the pattern set by [`TextArea::set_search_pattern`] forward and move the cursor to the next match
|
||||||
|
/// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
|
||||||
|
/// some match was found. Otherwise it returns `false`.
|
||||||
|
///
|
||||||
|
/// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
|
||||||
|
/// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
|
||||||
|
/// move to the next match ignoring the match at the current position.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
|
||||||
|
///
|
||||||
|
/// textarea.set_search_pattern("hello+").unwrap();
|
||||||
|
///
|
||||||
|
/// // Move to next position
|
||||||
|
/// let match_found = textarea.search_forward(false);
|
||||||
|
/// assert!(match_found);
|
||||||
|
/// assert_eq!(textarea.cursor(), (1, 0));
|
||||||
|
///
|
||||||
|
/// // Since the cursor position matches to "hello+", it does not move
|
||||||
|
/// textarea.search_forward(true);
|
||||||
|
/// assert_eq!(textarea.cursor(), (1, 0));
|
||||||
|
///
|
||||||
|
/// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
|
||||||
|
/// textarea.search_forward(false);
|
||||||
|
/// assert_eq!(textarea.cursor(), (2, 0));
|
||||||
|
///
|
||||||
|
/// // Text search wrap around the buffer
|
||||||
|
/// textarea.search_forward(false);
|
||||||
|
/// assert_eq!(textarea.cursor(), (0, 0));
|
||||||
|
///
|
||||||
|
/// // `false` is returned when no match was found
|
||||||
|
/// textarea.set_search_pattern("bye+").unwrap();
|
||||||
|
/// let match_found = textarea.search_forward(false);
|
||||||
|
/// assert!(!match_found);
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn search_forward(&mut self, match_cursor: bool) -> bool {
|
||||||
|
let pat = if let Some(pat) = &self.search_pat {
|
||||||
|
pat
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (row, col) = self.cursor;
|
||||||
|
let current_line = &self.lines[row];
|
||||||
|
|
||||||
|
// Search current line after cursor
|
||||||
|
let start_col = if match_cursor { col } else { col + 1 };
|
||||||
|
if let Some((i, _)) = current_line.char_indices().nth(start_col) {
|
||||||
|
if let Some(m) = pat.find_at(current_line, i) {
|
||||||
|
let col = start_col + current_line[i..m.start()].chars().count();
|
||||||
|
self.cursor = (row, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search lines after cursor
|
||||||
|
for (i, line) in self.lines[row + 1..].iter().enumerate() {
|
||||||
|
if let Some(m) = pat.find(line) {
|
||||||
|
let col = line[..m.start()].chars().count();
|
||||||
|
self.cursor = (row + 1 + i, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search lines before cursor (wrap)
|
||||||
|
for (i, line) in self.lines[..row].iter().enumerate() {
|
||||||
|
if let Some(m) = pat.find(line) {
|
||||||
|
let col = line[..m.start()].chars().count();
|
||||||
|
self.cursor = (i, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search current line before cursor
|
||||||
|
let col_idx = current_line
|
||||||
|
.char_indices()
|
||||||
|
.nth(col)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.unwrap_or(current_line.len());
|
||||||
|
if let Some(m) = pat.find(current_line) {
|
||||||
|
let i = m.start();
|
||||||
|
if i <= col_idx {
|
||||||
|
let col = current_line[..i].chars().count();
|
||||||
|
self.cursor = (row, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search the pattern set by [`TextArea::set_search_pattern`] backward and move the cursor to the next match
|
||||||
|
/// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
|
||||||
|
/// some match was found. Otherwise it returns `false`.
|
||||||
|
///
|
||||||
|
/// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
|
||||||
|
/// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
|
||||||
|
/// move to the next match ignoring the match at the current position.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
|
||||||
|
///
|
||||||
|
/// textarea.set_search_pattern("hello+").unwrap();
|
||||||
|
///
|
||||||
|
/// // Move to next position with wrapping around the text buffer
|
||||||
|
/// let match_found = textarea.search_back(false);
|
||||||
|
/// assert!(match_found);
|
||||||
|
/// assert_eq!(textarea.cursor(), (2, 0));
|
||||||
|
///
|
||||||
|
/// // Since the cursor position matches to "hello+", it does not move
|
||||||
|
/// textarea.search_back(true);
|
||||||
|
/// assert_eq!(textarea.cursor(), (2, 0));
|
||||||
|
///
|
||||||
|
/// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
|
||||||
|
/// textarea.search_back(false);
|
||||||
|
/// assert_eq!(textarea.cursor(), (1, 0));
|
||||||
|
///
|
||||||
|
/// // `false` is returned when no match was found
|
||||||
|
/// textarea.set_search_pattern("bye+").unwrap();
|
||||||
|
/// let match_found = textarea.search_back(false);
|
||||||
|
/// assert!(!match_found);
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn search_back(&mut self, match_cursor: bool) -> bool {
|
||||||
|
let pat = if let Some(pat) = &self.search_pat {
|
||||||
|
pat
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (row, col) = self.cursor;
|
||||||
|
let current_line = &self.lines[row];
|
||||||
|
|
||||||
|
// Search current line before cursor
|
||||||
|
if col > 0 || match_cursor {
|
||||||
|
let start_col = if match_cursor { col } else { col - 1 };
|
||||||
|
if let Some((i, _)) = current_line.char_indices().nth(start_col) {
|
||||||
|
if let Some(m) = pat
|
||||||
|
.find_iter(current_line)
|
||||||
|
.take_while(|m| m.start() <= i)
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
let col = current_line[..m.start()].chars().count();
|
||||||
|
self.cursor = (row, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search lines before cursor
|
||||||
|
for (i, line) in self.lines[..row].iter().enumerate().rev() {
|
||||||
|
if let Some(m) = pat.find_iter(line).last() {
|
||||||
|
let col = line[..m.start()].chars().count();
|
||||||
|
self.cursor = (i, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search lines after cursor (wrap)
|
||||||
|
for (i, line) in self.lines[row + 1..].iter().enumerate().rev() {
|
||||||
|
if let Some(m) = pat.find_iter(line).last() {
|
||||||
|
let col = line[..m.start()].chars().count();
|
||||||
|
self.cursor = (row + 1 + i, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search current line after cursor
|
||||||
|
if let Some((i, _)) = current_line.char_indices().nth(col) {
|
||||||
|
if let Some(m) = pat
|
||||||
|
.find_iter(current_line)
|
||||||
|
.skip_while(|m| m.start() < i)
|
||||||
|
.last()
|
||||||
|
{
|
||||||
|
let col = col + current_line[i..m.start()].chars().count();
|
||||||
|
self.cursor = (row, col);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text style at matches of text search. The default style is colored with blue in background.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tui::style::{Style, Color};
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let textarea = TextArea::default();
|
||||||
|
///
|
||||||
|
/// assert_eq!(textarea.search_style(), Style::default().bg(Color::Blue));
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn search_style(&self) -> Style {
|
||||||
|
self.search_style
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the text style at matches of text search. The default style is colored with blue in background.
|
||||||
|
/// ```
|
||||||
|
/// use tui::style::{Style, Color};
|
||||||
|
/// use tui_textarea::TextArea;
|
||||||
|
///
|
||||||
|
/// let mut textarea = TextArea::default();
|
||||||
|
///
|
||||||
|
/// let red_bg = Style::default().bg(Color::Red);
|
||||||
|
/// textarea.set_search_style(red_bg);
|
||||||
|
///
|
||||||
|
/// assert_eq!(textarea.search_style(), red_bg);
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
pub fn set_search_style(&mut self, style: Style) {
|
||||||
|
self.search_style = style;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Renderer<'a> {
|
struct Renderer<'a> {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user