mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 21:32:13 +03:00
Add scrollback search function
For the moment this is basic case sensitive substring matching, but it is extensible to case insentive and regex matching. refs: https://github.com/wez/wezterm/issues/91 refs: https://github.com/wez/wezterm/issues/106
This commit is contained in:
parent
81ededa9ac
commit
c1c6ef6ddb
@ -20,6 +20,9 @@ brief notes about them may accumulate here.
|
||||
`action=wezterm.action{ActivateTab=i-1}` to pass the integer argument.
|
||||
* Windows: now also available with a setup.exe installer
|
||||
* Added `ClearScrollback` key assignment to clear the scrollback. This is bound to CMD-K and CTRL-SHIFT-K by default.
|
||||
* Added `Search` key assignment to search the scrollback. This is bound to CMD-F and CTRL-SHIFT-F by default.
|
||||
It activates the search overlay; type (or paste) to enter a search pattern and highlight matches.
|
||||
Pressing Enter advances to the next match. Escape cancels the search overlay.
|
||||
|
||||
### 20200517-122836-92c201c6
|
||||
|
||||
|
@ -13,6 +13,7 @@ mod overlay;
|
||||
mod quad;
|
||||
mod renderstate;
|
||||
mod scrollbar;
|
||||
mod search;
|
||||
mod selection;
|
||||
mod tabbar;
|
||||
mod termwindow;
|
||||
|
346
src/frontend/gui/search.rs
Normal file
346
src/frontend/gui/search.rs
Normal file
@ -0,0 +1,346 @@
|
||||
use crate::frontend::gui::termwindow::TermWindow;
|
||||
use crate::mux::domain::DomainId;
|
||||
use crate::mux::renderable::*;
|
||||
use crate::mux::tab::{Pattern, SearchDirection, SearchResult};
|
||||
use crate::mux::tab::{Tab, TabId};
|
||||
use portable_pty::PtySize;
|
||||
use rangeset::RangeSet;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use term::color::ColorPalette;
|
||||
use term::{Clipboard, KeyCode, KeyModifiers, Line, MouseEvent, StableRowIndex, TerminalHost};
|
||||
use termwiz::cell::{Cell, CellAttributes};
|
||||
use termwiz::color::AnsiColor;
|
||||
use url::Url;
|
||||
use window::WindowOps;
|
||||
|
||||
pub struct SearchOverlay {
|
||||
renderer: RefCell<SearchRenderable>,
|
||||
delegate: Rc<dyn Tab>,
|
||||
}
|
||||
|
||||
struct MatchResult {
|
||||
range: Range<usize>,
|
||||
result_index: usize,
|
||||
}
|
||||
|
||||
struct SearchRenderable {
|
||||
delegate: Rc<dyn Tab>,
|
||||
/// The text that the user entered
|
||||
pattern: String,
|
||||
/// The most recently queried set of matches
|
||||
results: Vec<SearchResult>,
|
||||
by_line: HashMap<StableRowIndex, Vec<MatchResult>>,
|
||||
|
||||
viewport: Option<StableRowIndex>,
|
||||
last_bar_pos: Option<StableRowIndex>,
|
||||
|
||||
dirty_results: RangeSet<StableRowIndex>,
|
||||
result_pos: Option<usize>,
|
||||
|
||||
/// We use this to cancel ourselves later
|
||||
window: ::window::Window,
|
||||
}
|
||||
|
||||
impl SearchOverlay {
|
||||
pub fn with_tab(term_window: &TermWindow, tab: &Rc<dyn Tab>) -> Rc<dyn Tab> {
|
||||
let viewport = term_window.get_viewport(tab.tab_id());
|
||||
|
||||
let window = term_window.window.clone().unwrap();
|
||||
let mut renderer = SearchRenderable {
|
||||
delegate: Rc::clone(tab),
|
||||
pattern: String::new(),
|
||||
results: vec![],
|
||||
by_line: HashMap::new(),
|
||||
dirty_results: RangeSet::default(),
|
||||
viewport,
|
||||
last_bar_pos: None,
|
||||
window,
|
||||
result_pos: None,
|
||||
};
|
||||
|
||||
let search_row = renderer.compute_search_row();
|
||||
renderer.dirty_results.add(search_row);
|
||||
|
||||
Rc::new(SearchOverlay {
|
||||
renderer: RefCell::new(renderer),
|
||||
delegate: Rc::clone(tab),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn viewport_changed(&self, viewport: Option<StableRowIndex>) {
|
||||
let mut render = self.renderer.borrow_mut();
|
||||
if let Some(last) = render.last_bar_pos.take() {
|
||||
render.dirty_results.add(last);
|
||||
}
|
||||
if let Some(pos) = viewport.as_ref() {
|
||||
render.dirty_results.add(*pos);
|
||||
}
|
||||
render.viewport = viewport;
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab for SearchOverlay {
|
||||
fn tab_id(&self) -> TabId {
|
||||
self.delegate.tab_id()
|
||||
}
|
||||
|
||||
fn renderer(&self) -> RefMut<dyn Renderable> {
|
||||
self.renderer.borrow_mut()
|
||||
}
|
||||
|
||||
fn get_title(&self) -> String {
|
||||
self.delegate.get_title()
|
||||
}
|
||||
|
||||
fn send_paste(&self, text: &str) -> anyhow::Result<()> {
|
||||
// paste into the search bar
|
||||
let mut r = self.renderer.borrow_mut();
|
||||
r.pattern.push_str(text);
|
||||
r.update_search();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reader(&self) -> anyhow::Result<Box<dyn std::io::Read + Send>> {
|
||||
panic!("do not call reader on SearchOverlay bar tab instance");
|
||||
}
|
||||
|
||||
fn writer(&self) -> RefMut<dyn std::io::Write> {
|
||||
self.delegate.writer()
|
||||
}
|
||||
|
||||
fn resize(&self, size: PtySize) -> anyhow::Result<()> {
|
||||
self.delegate.resize(size)
|
||||
}
|
||||
|
||||
fn key_down(&self, key: KeyCode, _mods: KeyModifiers) -> anyhow::Result<()> {
|
||||
match key {
|
||||
KeyCode::Escape => self.renderer.borrow().close(),
|
||||
KeyCode::Enter => {
|
||||
let mut r = self.renderer.borrow_mut();
|
||||
if let Some(cur) = r.result_pos.as_ref() {
|
||||
let prior = if *cur > 0 {
|
||||
cur - 1
|
||||
} else {
|
||||
r.results.len() - 1
|
||||
};
|
||||
r.result_pos.replace(prior);
|
||||
r.set_viewport(Some(r.results[prior].start_y));
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut r = self.renderer.borrow_mut();
|
||||
r.pattern.push(c);
|
||||
r.update_search();
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let mut r = self.renderer.borrow_mut();
|
||||
r.pattern.pop();
|
||||
r.update_search();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mouse_event(&self, event: MouseEvent, host: &mut dyn TerminalHost) -> anyhow::Result<()> {
|
||||
self.delegate.mouse_event(event, host)
|
||||
}
|
||||
|
||||
fn advance_bytes(&self, buf: &[u8], host: &mut dyn TerminalHost) {
|
||||
self.delegate.advance_bytes(buf, host)
|
||||
}
|
||||
fn is_dead(&self) -> bool {
|
||||
self.delegate.is_dead()
|
||||
}
|
||||
|
||||
fn palette(&self) -> ColorPalette {
|
||||
self.delegate.palette()
|
||||
}
|
||||
fn domain_id(&self) -> DomainId {
|
||||
self.delegate.domain_id()
|
||||
}
|
||||
|
||||
fn erase_scrollback(&self) {
|
||||
self.delegate.erase_scrollback()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
_row: StableRowIndex,
|
||||
_direction: SearchDirection,
|
||||
_pattern: &Pattern,
|
||||
) -> Vec<SearchResult> {
|
||||
// You can't search the search bar
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn is_mouse_grabbed(&self) -> bool {
|
||||
// Force grabbing off while we're searching
|
||||
false
|
||||
}
|
||||
|
||||
fn set_clipboard(&self, clipboard: &Arc<dyn Clipboard>) {
|
||||
self.delegate.set_clipboard(clipboard)
|
||||
}
|
||||
|
||||
fn get_current_working_dir(&self) -> Option<Url> {
|
||||
self.delegate.get_current_working_dir()
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchRenderable {
|
||||
fn compute_search_row(&self) -> StableRowIndex {
|
||||
let dims = self.delegate.renderer().get_dimensions();
|
||||
let top = self.viewport.unwrap_or_else(|| dims.physical_top);
|
||||
let bottom = (top + dims.viewport_rows as StableRowIndex).saturating_sub(1);
|
||||
bottom
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
TermWindow::schedule_cancel_overlay(self.window.clone(), self.delegate.tab_id());
|
||||
}
|
||||
|
||||
fn set_viewport(&self, row: Option<StableRowIndex>) {
|
||||
let dims = self.delegate.renderer().get_dimensions();
|
||||
let tab_id = self.delegate.tab_id();
|
||||
self.window.apply(move |term_window, _window| {
|
||||
if let Some(term_window) = term_window.downcast_mut::<TermWindow>() {
|
||||
term_window.set_viewport(tab_id, row, dims);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn update_search(&mut self) {
|
||||
for idx in self.by_line.keys() {
|
||||
self.dirty_results.add(*idx);
|
||||
}
|
||||
if let Some(idx) = self.last_bar_pos.as_ref() {
|
||||
self.dirty_results.add(*idx);
|
||||
}
|
||||
|
||||
self.results.clear();
|
||||
self.by_line.clear();
|
||||
self.result_pos.take();
|
||||
|
||||
let bar_pos = self.compute_search_row();
|
||||
self.dirty_results.add(bar_pos);
|
||||
|
||||
if !self.pattern.is_empty() {
|
||||
self.results = self.delegate.search(
|
||||
bar_pos + 1,
|
||||
SearchDirection::Backwards,
|
||||
&Pattern::String(self.pattern.clone()),
|
||||
);
|
||||
self.results.sort();
|
||||
|
||||
let dims = self.get_dimensions();
|
||||
|
||||
for (result_index, res) in self.results.iter().enumerate() {
|
||||
for idx in res.start_y..=res.end_y {
|
||||
let range = if idx == res.start_y && idx == res.end_y {
|
||||
// Range on same line
|
||||
res.start_x..res.end_x
|
||||
} else if idx == res.end_y {
|
||||
// final line of multi-line
|
||||
0..res.end_x
|
||||
} else {
|
||||
// a middle line
|
||||
0..dims.cols
|
||||
};
|
||||
|
||||
let result = MatchResult {
|
||||
range,
|
||||
result_index,
|
||||
};
|
||||
|
||||
let matches = self.by_line.entry(idx).or_insert_with(|| vec![]);
|
||||
matches.push(result);
|
||||
|
||||
self.dirty_results.add(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(last) = self.results.last() {
|
||||
self.result_pos.replace(self.results.len() - 1);
|
||||
self.set_viewport(Some(last.start_y));
|
||||
} else {
|
||||
self.set_viewport(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for SearchRenderable {
|
||||
fn get_cursor_position(&self) -> StableCursorPosition {
|
||||
// move to the search box
|
||||
StableCursorPosition {
|
||||
x: 8 + self.pattern.len(), // FIXME: ucwidth
|
||||
y: self.compute_search_row(),
|
||||
shape: termwiz::surface::CursorShape::SteadyBlock,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dirty_lines(&self, lines: Range<StableRowIndex>) -> RangeSet<StableRowIndex> {
|
||||
let mut dirty = self.delegate.renderer().get_dirty_lines(lines.clone());
|
||||
dirty.add_set(&self.dirty_results);
|
||||
dirty.intersection_with_range(lines)
|
||||
}
|
||||
|
||||
fn get_lines(&mut self, lines: Range<StableRowIndex>) -> (StableRowIndex, Vec<Line>) {
|
||||
let dims = self.get_dimensions();
|
||||
|
||||
let (top, mut lines) = self.delegate.renderer().get_lines(lines);
|
||||
|
||||
// Process the lines; for the search row we want to render instead
|
||||
// the search UI.
|
||||
// For rows with search results, we want to highlight the matching ranges
|
||||
let search_row = self.compute_search_row();
|
||||
for (idx, line) in lines.iter_mut().enumerate() {
|
||||
let stable_idx = idx as StableRowIndex + top;
|
||||
self.dirty_results.remove(stable_idx);
|
||||
if stable_idx == search_row {
|
||||
// Replace with search UI
|
||||
let rev = CellAttributes::default().set_reverse(true).clone();
|
||||
line.fill_range(0..dims.cols, &Cell::new(' ', rev.clone()));
|
||||
line.overlay_text_with_attribute(
|
||||
0,
|
||||
&format!(
|
||||
"Search: {} ({}/{} matches)",
|
||||
self.pattern,
|
||||
self.result_pos.map(|x| x + 1).unwrap_or(0),
|
||||
self.results.len()
|
||||
),
|
||||
rev,
|
||||
);
|
||||
self.last_bar_pos = Some(search_row);
|
||||
} else if let Some(matches) = self.by_line.get(&stable_idx) {
|
||||
for m in matches {
|
||||
// highlight
|
||||
for cell in &mut line.cells_mut_for_attr_changes_only()[m.range.clone()] {
|
||||
if Some(m.result_index) == self.result_pos {
|
||||
cell.attrs_mut()
|
||||
.set_background(AnsiColor::Yellow)
|
||||
.set_foreground(AnsiColor::Black)
|
||||
.set_reverse(false);
|
||||
} else {
|
||||
cell.attrs_mut()
|
||||
.set_background(AnsiColor::Fuschia)
|
||||
.set_foreground(AnsiColor::Black)
|
||||
.set_reverse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(top, lines)
|
||||
}
|
||||
|
||||
fn get_dimensions(&self) -> RenderableDimensions {
|
||||
self.delegate.renderer().get_dimensions()
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ use crate::frontend::activity::Activity;
|
||||
use crate::frontend::front_end;
|
||||
use crate::frontend::gui::overlay::{launcher, start_overlay, tab_navigator};
|
||||
use crate::frontend::gui::scrollbar::*;
|
||||
use crate::frontend::gui::search::*;
|
||||
use crate::frontend::gui::selection::*;
|
||||
use crate::frontend::gui::tabbar::{TabBarItem, TabBarState};
|
||||
use crate::keyassignment::{
|
||||
@ -298,8 +299,7 @@ impl WindowCallbacks for TermWindow {
|
||||
|
||||
WMEK::VertWheel(amount) if !tab.is_mouse_grabbed() => {
|
||||
// adjust viewport
|
||||
let render = tab.renderer();
|
||||
let dims = render.get_dimensions();
|
||||
let dims = tab.renderer().get_dimensions();
|
||||
let position = self
|
||||
.get_viewport(tab.tab_id())
|
||||
.unwrap_or(dims.physical_top)
|
||||
@ -333,6 +333,7 @@ impl WindowCallbacks for TermWindow {
|
||||
self.terminal_size,
|
||||
&self.dimensions,
|
||||
);
|
||||
drop(render);
|
||||
self.set_viewport(tab.tab_id(), Some(row), dims);
|
||||
context.invalidate();
|
||||
return;
|
||||
@ -1144,6 +1145,7 @@ impl TermWindow {
|
||||
.get_viewport(tab.tab_id())
|
||||
.unwrap_or(dims.physical_top)
|
||||
.saturating_add(amount * dims.viewport_rows as isize);
|
||||
drop(render);
|
||||
self.set_viewport(tab.tab_id(), Some(position), dims);
|
||||
if let Some(win) = self.window.as_ref() {
|
||||
win.invalidate();
|
||||
@ -1474,6 +1476,10 @@ impl TermWindow {
|
||||
let window = self.window.as_ref().unwrap();
|
||||
window.invalidate();
|
||||
}
|
||||
Search => {
|
||||
let search = SearchOverlay::with_tab(self, tab);
|
||||
self.assign_overlay(tab.tab_id(), search);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@ -2642,11 +2648,11 @@ impl TermWindow {
|
||||
RefMut::map(self.tab_state(tab_id), |state| &mut state.selection)
|
||||
}
|
||||
|
||||
fn get_viewport(&self, tab_id: TabId) -> Option<StableRowIndex> {
|
||||
pub fn get_viewport(&self, tab_id: TabId) -> Option<StableRowIndex> {
|
||||
self.tab_state(tab_id).viewport
|
||||
}
|
||||
|
||||
fn set_viewport(
|
||||
pub fn set_viewport(
|
||||
&mut self,
|
||||
tab_id: TabId,
|
||||
position: Option<StableRowIndex>,
|
||||
@ -2663,7 +2669,17 @@ impl TermWindow {
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
self.tab_state(tab_id).viewport = pos;
|
||||
|
||||
let mut state = self.tab_state(tab_id);
|
||||
state.viewport = pos;
|
||||
|
||||
// This is a bit gross. If we add other overlays that need this information,
|
||||
// this should get extracted out into a trait
|
||||
if let Some(overlay) = state.overlay.as_ref() {
|
||||
if let Some(search_overlay) = overlay.downcast_ref::<SearchOverlay>() {
|
||||
search_overlay.viewport_changed(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_event_tab_bar(&mut self, x: usize, event: &MouseEvent, context: &dyn WindowOps) {
|
||||
@ -2709,13 +2725,16 @@ impl TermWindow {
|
||||
let dims = render.get_dimensions();
|
||||
let current_viewport = self.get_viewport(tab.tab_id());
|
||||
|
||||
match ScrollHit::test(
|
||||
let hit_result = ScrollHit::test(
|
||||
event.coords.y,
|
||||
&*render,
|
||||
current_viewport,
|
||||
self.terminal_size,
|
||||
&self.dimensions,
|
||||
) {
|
||||
);
|
||||
drop(render);
|
||||
|
||||
match hit_result {
|
||||
ScrollHit::Above => {
|
||||
// Page up
|
||||
self.set_viewport(
|
||||
|
@ -98,6 +98,7 @@ pub enum KeyAssignment {
|
||||
SpawnCommandInNewWindow(SpawnCommand),
|
||||
ShowLauncher,
|
||||
ClearScrollback,
|
||||
Search,
|
||||
|
||||
SelectTextAtMouseCursor(SelectionMode),
|
||||
ExtendSelectionToMouseCursor(Option<SelectionMode>),
|
||||
@ -161,6 +162,8 @@ impl InputMap {
|
||||
[ctrl_shift, KeyCode::Char('N'), SpawnWindow],
|
||||
[KeyModifiers::SUPER, KeyCode::Char('k'), ClearScrollback],
|
||||
[ctrl_shift, KeyCode::Char('K'), ClearScrollback],
|
||||
[KeyModifiers::SUPER, KeyCode::Char('f'), Search],
|
||||
[ctrl_shift, KeyCode::Char('F'), Search],
|
||||
// Font size manipulation
|
||||
[KeyModifiers::CTRL, KeyCode::Char('-'), DecreaseFontSize],
|
||||
[KeyModifiers::CTRL, KeyCode::Char('0'), ResetFontSize],
|
||||
|
@ -1,12 +1,13 @@
|
||||
use crate::mux::domain::DomainId;
|
||||
use crate::mux::renderable::Renderable;
|
||||
use crate::mux::tab::{alloc_tab_id, Tab, TabId};
|
||||
use crate::mux::tab::{Pattern, SearchDirection, SearchResult};
|
||||
use anyhow::Error;
|
||||
use portable_pty::{Child, MasterPty, PtySize};
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::sync::Arc;
|
||||
use term::color::ColorPalette;
|
||||
use term::{Clipboard, KeyCode, KeyModifiers, MouseEvent, Terminal, TerminalHost};
|
||||
use term::{Clipboard, KeyCode, KeyModifiers, MouseEvent, StableRowIndex, Terminal, TerminalHost};
|
||||
use url::Url;
|
||||
|
||||
pub struct LocalTab {
|
||||
@ -102,6 +103,80 @@ impl Tab for LocalTab {
|
||||
fn get_current_working_dir(&self) -> Option<Url> {
|
||||
self.terminal.borrow().get_current_dir().cloned()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
_row: StableRowIndex,
|
||||
_direction: SearchDirection,
|
||||
pattern: &Pattern,
|
||||
) -> Vec<SearchResult> {
|
||||
let term = self.terminal.borrow();
|
||||
let screen = term.screen();
|
||||
|
||||
let mut results = vec![];
|
||||
let mut haystack = String::new();
|
||||
let mut byte_pos_to_stable_idx = vec![];
|
||||
|
||||
fn haystack_idx_to_coord(
|
||||
idx: usize,
|
||||
byte_pos_to_stable_idx: &[(usize, StableRowIndex)],
|
||||
) -> (usize, StableRowIndex) {
|
||||
for (start, row) in byte_pos_to_stable_idx.iter().rev() {
|
||||
if idx >= *start {
|
||||
return (idx - *start, *row);
|
||||
}
|
||||
}
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn collect_matches(
|
||||
results: &mut Vec<SearchResult>,
|
||||
pattern: &Pattern,
|
||||
haystack: &str,
|
||||
byte_pos_to_stable_idx: &[(usize, StableRowIndex)],
|
||||
) {
|
||||
if haystack.is_empty() {
|
||||
return;
|
||||
}
|
||||
match pattern {
|
||||
Pattern::String(s) => {
|
||||
for (idx, s) in haystack.match_indices(s) {
|
||||
let (start_x, start_y) = haystack_idx_to_coord(idx, byte_pos_to_stable_idx);
|
||||
let (end_x, end_y) =
|
||||
haystack_idx_to_coord(idx + s.len(), byte_pos_to_stable_idx);
|
||||
results.push(SearchResult {
|
||||
start_x,
|
||||
start_y,
|
||||
end_x,
|
||||
end_y,
|
||||
});
|
||||
}
|
||||
} /*
|
||||
Pattern::Regex(r) => {
|
||||
// TODO
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, line) in screen.lines.iter().enumerate() {
|
||||
byte_pos_to_stable_idx.push((haystack.len(), screen.phys_to_stable_row_index(idx)));
|
||||
let mut wrapped = false;
|
||||
for (_, cell) in line.visible_cells() {
|
||||
haystack.push_str(cell.str());
|
||||
wrapped = cell.attrs().wrapped();
|
||||
}
|
||||
|
||||
if !wrapped {
|
||||
collect_matches(&mut results, pattern, &haystack, &byte_pos_to_stable_idx);
|
||||
haystack.clear();
|
||||
byte_pos_to_stable_idx.clear();
|
||||
}
|
||||
}
|
||||
|
||||
collect_matches(&mut results, pattern, &haystack, &byte_pos_to_stable_idx);
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalTab {
|
||||
|
@ -46,20 +46,21 @@ fn schedule_next_paste(paste: &Arc<Mutex<Paste>>) {
|
||||
|
||||
pub enum Pattern {
|
||||
String(String),
|
||||
Regex(regex::Regex),
|
||||
// Regex(regex::Regex),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SearchDirection {
|
||||
Backwards,
|
||||
Forwards,
|
||||
// Forwards,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub struct SearchResult {
|
||||
pub start_x: usize,
|
||||
pub start_y: StableRowIndex,
|
||||
pub end_x: usize,
|
||||
pub end_y: StableRowIndex,
|
||||
pub start_x: usize,
|
||||
pub end_x: usize,
|
||||
}
|
||||
|
||||
pub trait Tab: Downcast {
|
||||
|
Loading…
Reference in New Issue
Block a user