From 5914238724f9f75b853289aad88a4c6d7a9e5845 Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 30 Jun 2022 19:26:03 +0900 Subject: [PATCH 01/18] implement minimal text search - Set query with `TextArea::set_search_pattern` - Move cursor to next match `TextArea::search_forward` - Move cursor to previous match `TextArea::search_back` --- Cargo.toml | 4 +- src/textarea.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fbb1d83..38f78be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,11 @@ include = [ default = ["crossterm"] crossterm = ["dep:crossterm", "tui/crossterm"] termion = ["dep:termion", "tui/termion"] +search = ["dep:regex"] [dependencies] crossterm = { version = "0.23", optional = true } +regex = { version = "1", optional = true } termion = { version = "1.5", optional = true } tui = { version = "0.18", default-features = false } @@ -40,7 +42,7 @@ required-features = ["termion"] [[example]] name = "editor" -required-features = ["crossterm"] +required-features = ["crossterm", "search"] [[example]] name = "split" diff --git a/src/textarea.rs b/src/textarea.rs index 3a7c10a..38247a4 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -2,6 +2,8 @@ use crate::cursor::CursorMove; use crate::history::{Edit, EditKind, History}; use crate::input::{Input, Key}; use crate::word::{find_word_end_forward, find_word_start_backward}; +#[cfg(feature = "search")] +use regex::Regex; use std::borrow::Cow; use std::sync::atomic::{AtomicU16, Ordering}; use tui::buffer::Buffer; @@ -48,6 +50,8 @@ pub struct TextArea<'a> { scroll_top: (AtomicU16, AtomicU16), cursor_style: Style, yank: String, + #[cfg(feature = "search")] + search: Option, } /// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is @@ -142,6 +146,8 @@ impl<'a> TextArea<'a> { scroll_top: (AtomicU16::new(0), AtomicU16::new(0)), cursor_style: Style::default().add_modifier(Modifier::REVERSED), yank: String::new(), + #[cfg(feature = "search")] + search: None, } } @@ -1326,6 +1332,103 @@ impl<'a> TextArea<'a> { pub fn set_yank_text(&mut self, text: impl Into) { self.yank = text.into(); } + + #[cfg(feature = "search")] + pub fn set_search_pattern(&mut self, query: impl AsRef) -> Result<(), regex::Error> { + let query = query.as_ref(); + match &self.search { + Some(r) if r.as_str() == query => {} + _ if query.is_empty() => self.search = None, + _ => self.search = Some(Regex::new(query)?), + } + Ok(()) + } + + #[cfg(feature = "search")] + pub fn search_forward(&mut self) -> bool { + let pat = if let Some(pat) = &self.search { + pat + } else { + return false; + }; + let (row, col) = self.cursor; + + // Search current line + { + let line = &self.lines[row]; + // Avoid matching to current cursor position + if let Some((i, _)) = line.char_indices().nth(col + 1) { + if let Some(m) = pat.find_at(line, i) { + let col = col + 1 + 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; + } + } + + false + } + + #[cfg(feature = "search")] + pub fn search_back(&mut self) -> bool { + let pat = if let Some(pat) = &self.search { + pat + } else { + return false; + }; + let (row, col) = self.cursor; + + // Search current line + if col > 0 { + let line = &self.lines[row]; + // Avoid matching to current cursor position + if let Some((i, _)) = line.char_indices().nth(col - 1) { + if let Some(m) = pat.find_iter(line).take_while(|m| m.start() <= i).last() { + let col = 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(line) { + 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(line) { + let col = line[..m.start()].chars().count(); + self.cursor = (row + 1 + i, col); + return true; + } + } + + false + } } struct Renderer<'a> { From 68a18b2ae0db957ee4db451970d6220d5c460722 Mon Sep 17 00:00:00 2001 From: rhysd Date: Fri, 1 Jul 2022 23:07:02 +0900 Subject: [PATCH 02/18] highlight searched text --- src/textarea.rs | 170 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 131 insertions(+), 39 deletions(-) diff --git a/src/textarea.rs b/src/textarea.rs index 38247a4..1983c66 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -5,10 +5,11 @@ use crate::word::{find_word_end_forward, find_word_start_backward}; #[cfg(feature = "search")] use regex::Regex; use std::borrow::Cow; +use std::cmp; use std::sync::atomic::{AtomicU16, Ordering}; use tui::buffer::Buffer; use tui::layout::Rect; -use tui::style::{Modifier, Style}; +use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans, Text}; use tui::widgets::{Block, Paragraph, Widget}; @@ -17,6 +18,41 @@ fn spaces(size: u8) -> &'static str { &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