mirror of
https://github.com/wez/wezterm.git
synced 2024-11-23 23:21:08 +03:00
merge copy and search overlay code
The copy overlay now has a notion of running in search mode vs. copy mode; it can be launched in either mode. Search mode has a separate key table called `search_mode`. Activating copy mode while search mode is active will now update the mode of the existing overlay, rather than cancelling and creating a new instance, and vice versa. Activating copy mode while search mode is active will replace the current key table activation (which is assumed to be `copy_mode`) with `search_mode`, and vice versa. The viewport is no longer scrolled to the bottom when activating search mode. refs: https://github.com/wez/wezterm/issues/993 refs: https://github.com/wez/wezterm/issues/1592
This commit is contained in:
parent
865d857050
commit
2710cefb3e
@ -104,6 +104,12 @@ pub enum Pattern {
|
|||||||
Regex(String),
|
Regex(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Pattern {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::CaseSensitiveString("".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for Pattern {
|
impl std::ops::Deref for Pattern {
|
||||||
type Target = String;
|
type Target = String;
|
||||||
fn deref(&self) -> &String {
|
fn deref(&self) -> &String {
|
||||||
@ -391,6 +397,14 @@ pub enum CopyModeAssignment {
|
|||||||
PageUp,
|
PageUp,
|
||||||
PageDown,
|
PageDown,
|
||||||
Close,
|
Close,
|
||||||
|
PriorMatch,
|
||||||
|
NextMatch,
|
||||||
|
PriorMatchPage,
|
||||||
|
NextMatchPage,
|
||||||
|
CycleMatchType,
|
||||||
|
ClearPattern,
|
||||||
|
EditPattern,
|
||||||
|
AcceptPattern,
|
||||||
}
|
}
|
||||||
impl_lua_conversion!(CopyModeAssignment);
|
impl_lua_conversion!(CopyModeAssignment);
|
||||||
|
|
||||||
|
@ -176,7 +176,10 @@ impl InputMap {
|
|||||||
|
|
||||||
keys.by_name
|
keys.by_name
|
||||||
.entry("copy_mode".to_string())
|
.entry("copy_mode".to_string())
|
||||||
.or_insert_with(crate::overlay::copy::key_table);
|
.or_insert_with(crate::overlay::copy::copy_key_table);
|
||||||
|
keys.by_name
|
||||||
|
.entry("search_mode".to_string())
|
||||||
|
.or_insert_with(crate::overlay::copy::search_key_table);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
keys,
|
keys,
|
||||||
|
@ -4,15 +4,18 @@ use config::keyassignment::{
|
|||||||
CopyModeAssignment, KeyAssignment, KeyTable, KeyTableEntry, ScrollbackEraseMode,
|
CopyModeAssignment, KeyAssignment, KeyTable, KeyTableEntry, ScrollbackEraseMode,
|
||||||
};
|
};
|
||||||
use mux::domain::DomainId;
|
use mux::domain::DomainId;
|
||||||
use mux::pane::{Pane, PaneId};
|
use mux::pane::{Pane, PaneId, Pattern, SearchResult};
|
||||||
use mux::renderable::*;
|
use mux::renderable::*;
|
||||||
use portable_pty::PtySize;
|
use portable_pty::PtySize;
|
||||||
use rangeset::RangeSet;
|
use rangeset::RangeSet;
|
||||||
use std::cell::{RefCell, RefMut};
|
use std::cell::{RefCell, RefMut};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use termwiz::surface::{CursorVisibility, SequenceNo};
|
use termwiz::cell::{Cell, CellAttributes};
|
||||||
|
use termwiz::color::AnsiColor;
|
||||||
|
use termwiz::surface::{CursorVisibility, SequenceNo, SEQ_ZERO};
|
||||||
use unicode_segmentation::*;
|
use unicode_segmentation::*;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use wezterm_term::color::ColorPalette;
|
use wezterm_term::color::ColorPalette;
|
||||||
@ -33,6 +36,25 @@ struct CopyRenderable {
|
|||||||
viewport: Option<StableRowIndex>,
|
viewport: Option<StableRowIndex>,
|
||||||
/// We use this to cancel ourselves later
|
/// We use this to cancel ourselves later
|
||||||
window: ::window::Window,
|
window: ::window::Window,
|
||||||
|
|
||||||
|
/// The text that the user entered
|
||||||
|
pattern: Pattern,
|
||||||
|
/// The most recently queried set of matches
|
||||||
|
results: Vec<SearchResult>,
|
||||||
|
by_line: HashMap<StableRowIndex, Vec<MatchResult>>,
|
||||||
|
last_result_seqno: SequenceNo,
|
||||||
|
last_bar_pos: Option<StableRowIndex>,
|
||||||
|
dirty_results: RangeSet<StableRowIndex>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
editing_search: bool,
|
||||||
|
result_pos: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MatchResult {
|
||||||
|
range: Range<usize>,
|
||||||
|
result_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Dimensions {
|
struct Dimensions {
|
||||||
@ -41,33 +63,219 @@ struct Dimensions {
|
|||||||
top: StableRowIndex,
|
top: StableRowIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CopyModeParams {
|
||||||
|
pub pattern: Pattern,
|
||||||
|
pub editing_search: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl CopyOverlay {
|
impl CopyOverlay {
|
||||||
pub fn with_pane(term_window: &TermWindow, pane: &Rc<dyn Pane>) -> Rc<dyn Pane> {
|
pub fn with_pane(
|
||||||
|
term_window: &TermWindow,
|
||||||
|
pane: &Rc<dyn Pane>,
|
||||||
|
params: CopyModeParams,
|
||||||
|
) -> Rc<dyn Pane> {
|
||||||
let mut cursor = pane.get_cursor_position();
|
let mut cursor = pane.get_cursor_position();
|
||||||
cursor.shape = termwiz::surface::CursorShape::SteadyBlock;
|
cursor.shape = termwiz::surface::CursorShape::SteadyBlock;
|
||||||
cursor.visibility = CursorVisibility::Visible;
|
cursor.visibility = CursorVisibility::Visible;
|
||||||
|
|
||||||
let window = term_window.window.clone().unwrap();
|
let window = term_window.window.clone().unwrap();
|
||||||
let render = CopyRenderable {
|
let dims = pane.get_dimensions();
|
||||||
|
let mut render = CopyRenderable {
|
||||||
cursor,
|
cursor,
|
||||||
window,
|
window,
|
||||||
delegate: Rc::clone(pane),
|
delegate: Rc::clone(pane),
|
||||||
start: None,
|
start: None,
|
||||||
viewport: term_window.get_viewport(pane.pane_id()),
|
viewport: term_window.get_viewport(pane.pane_id()),
|
||||||
|
results: vec![],
|
||||||
|
by_line: HashMap::new(),
|
||||||
|
dirty_results: RangeSet::default(),
|
||||||
|
width: dims.cols,
|
||||||
|
height: dims.viewport_rows,
|
||||||
|
last_result_seqno: SEQ_ZERO,
|
||||||
|
last_bar_pos: None,
|
||||||
|
pattern: params.pattern,
|
||||||
|
editing_search: params.editing_search,
|
||||||
|
result_pos: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let search_row = render.compute_search_row();
|
||||||
|
render.dirty_results.add(search_row);
|
||||||
|
render.update_search();
|
||||||
|
|
||||||
Rc::new(CopyOverlay {
|
Rc::new(CopyOverlay {
|
||||||
delegate: Rc::clone(pane),
|
delegate: Rc::clone(pane),
|
||||||
render: RefCell::new(render),
|
render: RefCell::new(render),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_params(&self) -> CopyModeParams {
|
||||||
|
let render = self.render.borrow();
|
||||||
|
CopyModeParams {
|
||||||
|
pattern: render.pattern.clone(),
|
||||||
|
editing_search: render.editing_search,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_params(&self, params: CopyModeParams) {
|
||||||
|
let mut render = self.render.borrow_mut();
|
||||||
|
render.editing_search = params.editing_search;
|
||||||
|
if render.pattern != params.pattern {
|
||||||
|
render.pattern = params.pattern;
|
||||||
|
render.update_search();
|
||||||
|
}
|
||||||
|
let search_row = render.compute_search_row();
|
||||||
|
render.dirty_results.add(search_row);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn viewport_changed(&self, viewport: Option<StableRowIndex>) {
|
pub fn viewport_changed(&self, viewport: Option<StableRowIndex>) {
|
||||||
let mut r = self.render.borrow_mut();
|
let mut render = self.render.borrow_mut();
|
||||||
r.viewport = viewport;
|
if render.viewport != viewport {
|
||||||
|
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 CopyRenderable {
|
impl CopyRenderable {
|
||||||
|
fn compute_search_row(&self) -> StableRowIndex {
|
||||||
|
let dims = self.delegate.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 check_for_resize(&mut self) {
|
||||||
|
let dims = self.delegate.get_dimensions();
|
||||||
|
if dims.cols == self.width && dims.viewport_rows == self.height {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.width = dims.cols;
|
||||||
|
self.height = dims.viewport_rows;
|
||||||
|
|
||||||
|
let pos = self.result_pos;
|
||||||
|
self.update_search();
|
||||||
|
self.result_pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recompute_results(&mut self) {
|
||||||
|
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 if idx == res.start_y {
|
||||||
|
// first line of multi-line
|
||||||
|
res.start_x..self.width
|
||||||
|
} else {
|
||||||
|
// a middle line
|
||||||
|
0..self.width
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
self.last_result_seqno = self.delegate.get_current_seqno();
|
||||||
|
|
||||||
|
if !self.pattern.is_empty() {
|
||||||
|
let pane: Rc<dyn Pane> = self.delegate.clone();
|
||||||
|
let window = self.window.clone();
|
||||||
|
let pattern = self.pattern.clone();
|
||||||
|
promise::spawn::spawn(async move {
|
||||||
|
let mut results = pane.search(pattern).await?;
|
||||||
|
results.sort();
|
||||||
|
|
||||||
|
let pane_id = pane.pane_id();
|
||||||
|
let mut results = Some(results);
|
||||||
|
window.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
||||||
|
let state = term_window.pane_state(pane_id);
|
||||||
|
if let Some(overlay) = state.overlay.as_ref() {
|
||||||
|
if let Some(copy_overlay) = overlay.pane.downcast_ref::<CopyOverlay>() {
|
||||||
|
let mut r = copy_overlay.render.borrow_mut();
|
||||||
|
r.results = results.take().unwrap();
|
||||||
|
r.recompute_results();
|
||||||
|
let num_results = r.results.len();
|
||||||
|
|
||||||
|
if !r.results.is_empty() {
|
||||||
|
r.activate_match_number(num_results - 1);
|
||||||
|
} else {
|
||||||
|
r.set_viewport(None);
|
||||||
|
r.clear_selection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
anyhow::Result::<()>::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
} else {
|
||||||
|
self.clear_selection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_selection(&mut self) {
|
||||||
|
let pane_id = self.delegate.pane_id();
|
||||||
|
self.window
|
||||||
|
.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
||||||
|
let mut selection = term_window.selection(pane_id);
|
||||||
|
selection.origin.take();
|
||||||
|
selection.range.take();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_match_number(&mut self, n: usize) {
|
||||||
|
self.result_pos.replace(n);
|
||||||
|
let result = self.results[n].clone();
|
||||||
|
self.cursor.y = result.end_y;
|
||||||
|
self.cursor.x = result.end_x.saturating_sub(1);
|
||||||
|
|
||||||
|
let start = SelectionCoordinate {
|
||||||
|
x: result.start_x,
|
||||||
|
y: result.start_y,
|
||||||
|
};
|
||||||
|
let end = SelectionCoordinate {
|
||||||
|
// inclusive range for selection, but the result
|
||||||
|
// range is exclusive
|
||||||
|
x: result.end_x.saturating_sub(1),
|
||||||
|
y: result.end_y,
|
||||||
|
};
|
||||||
|
self.start.replace(start);
|
||||||
|
self.adjust_selection(start, SelectionRange { start, end });
|
||||||
|
}
|
||||||
|
|
||||||
fn clamp_cursor_to_scrollback(&mut self) {
|
fn clamp_cursor_to_scrollback(&mut self) {
|
||||||
let dims = self.delegate.get_dimensions();
|
let dims = self.delegate.get_dimensions();
|
||||||
if self.cursor.x >= dims.cols {
|
if self.cursor.x >= dims.cols {
|
||||||
@ -180,6 +388,87 @@ impl CopyRenderable {
|
|||||||
self.select_to_cursor_pos();
|
self.select_to_cursor_pos();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move to prior match
|
||||||
|
fn prior_match(&mut self) {
|
||||||
|
if let Some(cur) = self.result_pos.as_ref() {
|
||||||
|
let prior = if *cur > 0 {
|
||||||
|
cur - 1
|
||||||
|
} else {
|
||||||
|
self.results.len() - 1
|
||||||
|
};
|
||||||
|
self.activate_match_number(prior);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move to next match
|
||||||
|
fn next_match(&mut self) {
|
||||||
|
if let Some(cur) = self.result_pos.as_ref() {
|
||||||
|
let next = if *cur + 1 >= self.results.len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
*cur + 1
|
||||||
|
};
|
||||||
|
self.activate_match_number(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Skip this page of matches and move up to the first match from
|
||||||
|
/// the prior page.
|
||||||
|
fn prior_match_page(&mut self) {
|
||||||
|
let dims = self.delegate.get_dimensions();
|
||||||
|
if let Some(cur) = self.result_pos {
|
||||||
|
let top = self.viewport.unwrap_or(dims.physical_top);
|
||||||
|
let prior = top - dims.viewport_rows as isize;
|
||||||
|
if let Some(pos) = self
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.position(|res| res.start_y > prior && res.start_y < top)
|
||||||
|
{
|
||||||
|
self.activate_match_number(pos);
|
||||||
|
} else {
|
||||||
|
self.activate_match_number(cur.saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip this page of matches and move down to the first match from
|
||||||
|
/// the next page.
|
||||||
|
fn next_match_page(&mut self) {
|
||||||
|
let dims = self.delegate.get_dimensions();
|
||||||
|
if let Some(cur) = self.result_pos {
|
||||||
|
let top = self.viewport.unwrap_or(dims.physical_top);
|
||||||
|
let bottom = top + dims.viewport_rows as isize;
|
||||||
|
if let Some(pos) = self.results.iter().position(|res| res.start_y >= bottom) {
|
||||||
|
self.activate_match_number(pos);
|
||||||
|
} else {
|
||||||
|
let len = self.results.len().saturating_sub(1);
|
||||||
|
self.activate_match_number(cur.min(len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_pattern(&mut self) {
|
||||||
|
self.pattern.clear();
|
||||||
|
self.update_search();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_pattern(&mut self) {
|
||||||
|
self.editing_search = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_pattern(&mut self) {
|
||||||
|
self.editing_search = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle_match_type(&mut self) {
|
||||||
|
let pattern = match &self.pattern {
|
||||||
|
Pattern::CaseSensitiveString(s) => Pattern::CaseInSensitiveString(s.clone()),
|
||||||
|
Pattern::CaseInSensitiveString(s) => Pattern::Regex(s.clone()),
|
||||||
|
Pattern::Regex(s) => Pattern::CaseSensitiveString(s.clone()),
|
||||||
|
};
|
||||||
|
self.pattern = pattern;
|
||||||
|
self.update_search();
|
||||||
|
}
|
||||||
|
|
||||||
fn move_to_viewport_middle(&mut self) {
|
fn move_to_viewport_middle(&mut self) {
|
||||||
let dims = self.dimensions();
|
let dims = self.dimensions();
|
||||||
self.cursor.y = dims.top + (dims.dims.viewport_rows as isize) / 2;
|
self.cursor.y = dims.top + (dims.dims.viewport_rows as isize) / 2;
|
||||||
@ -386,8 +675,12 @@ impl Pane for CopyOverlay {
|
|||||||
format!("Copy mode: {}", self.delegate.get_title())
|
format!("Copy mode: {}", self.delegate.get_title())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_paste(&self, _text: &str) -> anyhow::Result<()> {
|
fn send_paste(&self, text: &str) -> anyhow::Result<()> {
|
||||||
anyhow::bail!("ignoring paste while copying");
|
// paste into the search bar
|
||||||
|
let mut r = self.render.borrow_mut();
|
||||||
|
r.pattern.push_str(text);
|
||||||
|
r.update_search();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reader(&self) -> anyhow::Result<Option<Box<dyn std::io::Read + Send>>> {
|
fn reader(&self) -> anyhow::Result<Option<Box<dyn std::io::Read + Send>>> {
|
||||||
@ -406,6 +699,28 @@ impl Pane for CopyOverlay {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn key_down(&self, key: KeyCode, mods: KeyModifiers) -> anyhow::Result<()> {
|
||||||
|
let mut render = self.render.borrow_mut();
|
||||||
|
if render.editing_search {
|
||||||
|
match (key, mods) {
|
||||||
|
(KeyCode::Char(c), KeyModifiers::NONE)
|
||||||
|
| (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
||||||
|
// Type to add to the pattern
|
||||||
|
render.pattern.push(c);
|
||||||
|
render.update_search();
|
||||||
|
}
|
||||||
|
(KeyCode::Backspace, KeyModifiers::NONE) => {
|
||||||
|
// Backspace to edit the pattern
|
||||||
|
render.pattern.pop();
|
||||||
|
render.update_search();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn perform_assignment(&self, assignment: &KeyAssignment) -> bool {
|
fn perform_assignment(&self, assignment: &KeyAssignment) -> bool {
|
||||||
use CopyModeAssignment::*;
|
use CopyModeAssignment::*;
|
||||||
match assignment {
|
match assignment {
|
||||||
@ -431,6 +746,14 @@ impl Pane for CopyOverlay {
|
|||||||
PageUp => render.page_up(),
|
PageUp => render.page_up(),
|
||||||
PageDown => render.page_down(),
|
PageDown => render.page_down(),
|
||||||
Close => render.close(),
|
Close => render.close(),
|
||||||
|
PriorMatch => render.prior_match(),
|
||||||
|
NextMatch => render.next_match(),
|
||||||
|
PriorMatchPage => render.prior_match_page(),
|
||||||
|
NextMatchPage => render.next_match_page(),
|
||||||
|
CycleMatchType => render.cycle_match_type(),
|
||||||
|
ClearPattern => render.clear_pattern(),
|
||||||
|
EditPattern => render.edit_pattern(),
|
||||||
|
AcceptPattern => render.accept_pattern(),
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -438,10 +761,6 @@ impl Pane for CopyOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_down(&self, _key: KeyCode, _mods: KeyModifiers) -> anyhow::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse_event(&self, _event: MouseEvent) -> anyhow::Result<()> {
|
fn mouse_event(&self, _event: MouseEvent) -> anyhow::Result<()> {
|
||||||
anyhow::bail!("ignoring mouse while copying");
|
anyhow::bail!("ignoring mouse while copying");
|
||||||
}
|
}
|
||||||
@ -484,7 +803,18 @@ impl Pane for CopyOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_cursor_position(&self) -> StableCursorPosition {
|
fn get_cursor_position(&self) -> StableCursorPosition {
|
||||||
self.render.borrow().cursor
|
let renderer = self.render.borrow();
|
||||||
|
if renderer.editing_search {
|
||||||
|
// place in the search box
|
||||||
|
StableCursorPosition {
|
||||||
|
x: 8 + wezterm_term::unicode_column_width(&renderer.pattern, None),
|
||||||
|
y: renderer.compute_search_row(),
|
||||||
|
shape: termwiz::surface::CursorShape::SteadyBlock,
|
||||||
|
visibility: termwiz::surface::CursorVisibility::Visible,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderer.cursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_seqno(&self) -> SequenceNo {
|
fn get_current_seqno(&self) -> SequenceNo {
|
||||||
@ -500,7 +830,70 @@ impl Pane for CopyOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_lines(&self, lines: Range<StableRowIndex>) -> (StableRowIndex, Vec<Line>) {
|
fn get_lines(&self, lines: Range<StableRowIndex>) -> (StableRowIndex, Vec<Line>) {
|
||||||
self.delegate.get_lines(lines)
|
let mut renderer = self.render.borrow_mut();
|
||||||
|
if self.delegate.get_current_seqno() > renderer.last_result_seqno {
|
||||||
|
renderer.update_search();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.check_for_resize();
|
||||||
|
let dims = self.get_dimensions();
|
||||||
|
|
||||||
|
let (top, mut lines) = self.delegate.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 = renderer.compute_search_row();
|
||||||
|
for (idx, line) in lines.iter_mut().enumerate() {
|
||||||
|
let stable_idx = idx as StableRowIndex + top;
|
||||||
|
renderer.dirty_results.remove(stable_idx);
|
||||||
|
if stable_idx == search_row && (renderer.editing_search || !renderer.pattern.is_empty())
|
||||||
|
{
|
||||||
|
// Replace with search UI
|
||||||
|
let rev = CellAttributes::default().set_reverse(true).clone();
|
||||||
|
line.fill_range(0..dims.cols, &Cell::new(' ', rev.clone()), SEQ_ZERO);
|
||||||
|
let mode = &match renderer.pattern {
|
||||||
|
Pattern::CaseSensitiveString(_) => "case-sensitive",
|
||||||
|
Pattern::CaseInSensitiveString(_) => "ignore-case",
|
||||||
|
Pattern::Regex(_) => "regex",
|
||||||
|
};
|
||||||
|
line.overlay_text_with_attribute(
|
||||||
|
0,
|
||||||
|
&format!(
|
||||||
|
"Search: {} ({}/{} matches. {})",
|
||||||
|
*renderer.pattern,
|
||||||
|
renderer.result_pos.map(|x| x + 1).unwrap_or(0),
|
||||||
|
renderer.results.len(),
|
||||||
|
mode
|
||||||
|
),
|
||||||
|
rev,
|
||||||
|
SEQ_ZERO,
|
||||||
|
);
|
||||||
|
renderer.last_bar_pos = Some(search_row);
|
||||||
|
} else if let Some(matches) = renderer.by_line.get(&stable_idx) {
|
||||||
|
for m in matches {
|
||||||
|
// highlight
|
||||||
|
for cell_idx in m.range.clone() {
|
||||||
|
if let Some(cell) = line.cells_mut_for_attr_changes_only().get_mut(cell_idx)
|
||||||
|
{
|
||||||
|
if Some(m.result_index) == renderer.result_pos {
|
||||||
|
cell.attrs_mut()
|
||||||
|
.set_background(AnsiColor::Yellow)
|
||||||
|
.set_foreground(AnsiColor::Black)
|
||||||
|
.set_reverse(false);
|
||||||
|
} else {
|
||||||
|
cell.attrs_mut()
|
||||||
|
.set_background(AnsiColor::Fuchsia)
|
||||||
|
.set_foreground(AnsiColor::Black)
|
||||||
|
.set_reverse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(top, lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_dimensions(&self) -> RenderableDimensions {
|
fn get_dimensions(&self) -> RenderableDimensions {
|
||||||
@ -516,7 +909,66 @@ fn is_whitespace_word(word: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key_table() -> KeyTable {
|
pub fn search_key_table() -> KeyTable {
|
||||||
|
let mut table = KeyTable::default();
|
||||||
|
for (key, mods, action) in [
|
||||||
|
(
|
||||||
|
WKeyCode::Char('\x1b'),
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::Close),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::UpArrow,
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::PriorMatch),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::Char('\r'),
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::PriorMatch),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::Char('p'),
|
||||||
|
Modifiers::CTRL,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::PriorMatch),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::PageUp,
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::PriorMatchPage),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::PageDown,
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::NextMatchPage),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::Char('n'),
|
||||||
|
Modifiers::CTRL,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::NextMatch),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::DownArrow,
|
||||||
|
Modifiers::NONE,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::NextMatch),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::Char('r'),
|
||||||
|
Modifiers::CTRL,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::CycleMatchType),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
WKeyCode::Char('u'),
|
||||||
|
Modifiers::CTRL,
|
||||||
|
KeyAssignment::CopyMode(CopyModeAssignment::ClearPattern),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
table.insert((key, mods), KeyTableEntry { action });
|
||||||
|
}
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_key_table() -> KeyTable {
|
||||||
let mut table = KeyTable::default();
|
let mut table = KeyTable::default();
|
||||||
for (key, mods, action) in [
|
for (key, mods, action) in [
|
||||||
(
|
(
|
||||||
@ -625,7 +1077,7 @@ pub fn key_table() -> KeyTable {
|
|||||||
KeyAssignment::CopyMode(CopyModeAssignment::MoveToStartOfLine),
|
KeyAssignment::CopyMode(CopyModeAssignment::MoveToStartOfLine),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
WKeyCode::Char('\n'),
|
WKeyCode::Char('\r'),
|
||||||
Modifiers::NONE,
|
Modifiers::NONE,
|
||||||
KeyAssignment::CopyMode(CopyModeAssignment::MoveToStartOfNextLine),
|
KeyAssignment::CopyMode(CopyModeAssignment::MoveToStartOfNextLine),
|
||||||
),
|
),
|
||||||
|
@ -11,16 +11,14 @@ pub mod copy;
|
|||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod launcher;
|
pub mod launcher;
|
||||||
pub mod quickselect;
|
pub mod quickselect;
|
||||||
pub mod search;
|
|
||||||
|
|
||||||
pub use confirm_close_pane::{
|
pub use confirm_close_pane::{
|
||||||
confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program,
|
confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program,
|
||||||
};
|
};
|
||||||
pub use copy::CopyOverlay;
|
pub use copy::{CopyModeParams, CopyOverlay};
|
||||||
pub use debug::show_debug_overlay;
|
pub use debug::show_debug_overlay;
|
||||||
pub use launcher::{launcher, LauncherArgs, LauncherFlags};
|
pub use launcher::{launcher, LauncherArgs, LauncherFlags};
|
||||||
pub use quickselect::QuickSelectOverlay;
|
pub use quickselect::QuickSelectOverlay;
|
||||||
pub use search::SearchOverlay;
|
|
||||||
|
|
||||||
pub fn start_overlay<T, F>(
|
pub fn start_overlay<T, F>(
|
||||||
term_window: &TermWindow,
|
term_window: &TermWindow,
|
||||||
|
@ -1,524 +0,0 @@
|
|||||||
use crate::selection::{SelectionCoordinate, SelectionRange};
|
|
||||||
use crate::termwindow::{TermWindow, TermWindowNotif};
|
|
||||||
use config::keyassignment::ScrollbackEraseMode;
|
|
||||||
use mux::domain::DomainId;
|
|
||||||
use mux::pane::{Pane, PaneId, Pattern, SearchResult};
|
|
||||||
use mux::renderable::*;
|
|
||||||
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 termwiz::cell::{Cell, CellAttributes};
|
|
||||||
use termwiz::color::AnsiColor;
|
|
||||||
use termwiz::surface::{SequenceNo, SEQ_ZERO};
|
|
||||||
use url::Url;
|
|
||||||
use wezterm_term::color::ColorPalette;
|
|
||||||
use wezterm_term::{Clipboard, KeyCode, KeyModifiers, Line, MouseEvent, StableRowIndex};
|
|
||||||
use window::WindowOps;
|
|
||||||
|
|
||||||
pub struct SearchOverlay {
|
|
||||||
renderer: RefCell<SearchRenderable>,
|
|
||||||
delegate: Rc<dyn Pane>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct MatchResult {
|
|
||||||
range: Range<usize>,
|
|
||||||
result_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SearchRenderable {
|
|
||||||
delegate: Rc<dyn Pane>,
|
|
||||||
/// The text that the user entered
|
|
||||||
pattern: Pattern,
|
|
||||||
/// The most recently queried set of matches
|
|
||||||
results: Vec<SearchResult>,
|
|
||||||
by_line: HashMap<StableRowIndex, Vec<MatchResult>>,
|
|
||||||
last_result_seqno: SequenceNo,
|
|
||||||
|
|
||||||
viewport: Option<StableRowIndex>,
|
|
||||||
last_bar_pos: Option<StableRowIndex>,
|
|
||||||
|
|
||||||
dirty_results: RangeSet<StableRowIndex>,
|
|
||||||
result_pos: Option<usize>,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
|
|
||||||
/// We use this to cancel ourselves later
|
|
||||||
window: ::window::Window,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchOverlay {
|
|
||||||
pub fn with_pane(
|
|
||||||
term_window: &TermWindow,
|
|
||||||
pane: &Rc<dyn Pane>,
|
|
||||||
pattern: Pattern,
|
|
||||||
) -> Rc<dyn Pane> {
|
|
||||||
let viewport = term_window.get_viewport(pane.pane_id());
|
|
||||||
let dims = pane.get_dimensions();
|
|
||||||
|
|
||||||
let window = term_window.window.clone().unwrap();
|
|
||||||
let mut renderer = SearchRenderable {
|
|
||||||
delegate: Rc::clone(pane),
|
|
||||||
pattern,
|
|
||||||
results: vec![],
|
|
||||||
by_line: HashMap::new(),
|
|
||||||
dirty_results: RangeSet::default(),
|
|
||||||
viewport,
|
|
||||||
last_bar_pos: None,
|
|
||||||
last_result_seqno: SEQ_ZERO,
|
|
||||||
window,
|
|
||||||
result_pos: None,
|
|
||||||
width: dims.cols,
|
|
||||||
height: dims.viewport_rows,
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_row = renderer.compute_search_row();
|
|
||||||
renderer.dirty_results.add(search_row);
|
|
||||||
renderer.update_search();
|
|
||||||
|
|
||||||
Rc::new(SearchOverlay {
|
|
||||||
renderer: RefCell::new(renderer),
|
|
||||||
delegate: Rc::clone(pane),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn viewport_changed(&self, viewport: Option<StableRowIndex>) {
|
|
||||||
let mut render = self.renderer.borrow_mut();
|
|
||||||
if render.viewport != viewport {
|
|
||||||
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 Pane for SearchOverlay {
|
|
||||||
fn pane_id(&self) -> PaneId {
|
|
||||||
self.delegate.pane_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Option<Box<dyn std::io::Read + Send>>> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writer(&self) -> RefMut<dyn std::io::Write> {
|
|
||||||
self.delegate.writer()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize(&self, size: PtySize) -> anyhow::Result<()> {
|
|
||||||
self.delegate.resize(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_up(&self, _key: KeyCode, _mods: KeyModifiers) -> anyhow::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_down(&self, key: KeyCode, mods: KeyModifiers) -> anyhow::Result<()> {
|
|
||||||
match (key, mods) {
|
|
||||||
(KeyCode::Escape, KeyModifiers::NONE) => self.renderer.borrow().close(),
|
|
||||||
(KeyCode::UpArrow, KeyModifiers::NONE)
|
|
||||||
| (KeyCode::Enter, KeyModifiers::NONE)
|
|
||||||
| (KeyCode::Char('p'), KeyModifiers::CTRL) => {
|
|
||||||
// Move to prior match
|
|
||||||
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.activate_match_number(prior);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(KeyCode::PageUp, KeyModifiers::NONE) => {
|
|
||||||
// Skip this page of matches and move up to the first match from
|
|
||||||
// the prior page.
|
|
||||||
let dims = self.delegate.get_dimensions();
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
if let Some(cur) = r.result_pos {
|
|
||||||
let top = r.viewport.unwrap_or(dims.physical_top);
|
|
||||||
let prior = top - dims.viewport_rows as isize;
|
|
||||||
if let Some(pos) = r
|
|
||||||
.results
|
|
||||||
.iter()
|
|
||||||
.position(|res| res.start_y > prior && res.start_y < top)
|
|
||||||
{
|
|
||||||
r.activate_match_number(pos);
|
|
||||||
} else {
|
|
||||||
r.activate_match_number(cur.saturating_sub(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(KeyCode::PageDown, KeyModifiers::NONE) => {
|
|
||||||
// Skip this page of matches and move down to the first match from
|
|
||||||
// the next page.
|
|
||||||
let dims = self.delegate.get_dimensions();
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
if let Some(cur) = r.result_pos {
|
|
||||||
let top = r.viewport.unwrap_or(dims.physical_top);
|
|
||||||
let bottom = top + dims.viewport_rows as isize;
|
|
||||||
if let Some(pos) = r.results.iter().position(|res| res.start_y >= bottom) {
|
|
||||||
r.activate_match_number(pos);
|
|
||||||
} else {
|
|
||||||
let len = r.results.len().saturating_sub(1);
|
|
||||||
r.activate_match_number(cur.min(len));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(KeyCode::DownArrow, KeyModifiers::NONE) | (KeyCode::Char('n'), KeyModifiers::CTRL) => {
|
|
||||||
// Move to next match
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
if let Some(cur) = r.result_pos.as_ref() {
|
|
||||||
let next = if *cur + 1 >= r.results.len() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
*cur + 1
|
|
||||||
};
|
|
||||||
r.activate_match_number(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(KeyCode::Char('r'), KeyModifiers::CTRL) => {
|
|
||||||
// CTRL-r cycles through pattern match types
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
let pattern = match &r.pattern {
|
|
||||||
Pattern::CaseSensitiveString(s) => Pattern::CaseInSensitiveString(s.clone()),
|
|
||||||
Pattern::CaseInSensitiveString(s) => Pattern::Regex(s.clone()),
|
|
||||||
Pattern::Regex(s) => Pattern::CaseSensitiveString(s.clone()),
|
|
||||||
};
|
|
||||||
r.pattern = pattern;
|
|
||||||
r.update_search();
|
|
||||||
}
|
|
||||||
(KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
|
||||||
// Type to add to the pattern
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
r.pattern.push(c);
|
|
||||||
r.update_search();
|
|
||||||
}
|
|
||||||
(KeyCode::Backspace, KeyModifiers::NONE) => {
|
|
||||||
// Backspace to edit the pattern
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
r.pattern.pop();
|
|
||||||
r.update_search();
|
|
||||||
}
|
|
||||||
(KeyCode::Char('u'), KeyModifiers::CTRL) => {
|
|
||||||
// CTRL-u to clear the pattern
|
|
||||||
let mut r = self.renderer.borrow_mut();
|
|
||||||
r.pattern.clear();
|
|
||||||
r.update_search();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse_event(&self, event: MouseEvent) -> anyhow::Result<()> {
|
|
||||||
self.delegate.mouse_event(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_actions(&self, actions: Vec<termwiz::escape::Action>) {
|
|
||||||
self.delegate.perform_actions(actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, erase_mode: ScrollbackEraseMode) {
|
|
||||||
self.delegate.erase_scrollback(erase_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_mouse_grabbed(&self) -> bool {
|
|
||||||
// Force grabbing off while we're searching
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_alt_screen_active(&self) -> bool {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cursor_position(&self) -> StableCursorPosition {
|
|
||||||
// move to the search box
|
|
||||||
let renderer = self.renderer.borrow();
|
|
||||||
StableCursorPosition {
|
|
||||||
x: 8 + wezterm_term::unicode_column_width(&renderer.pattern, None),
|
|
||||||
y: renderer.compute_search_row(),
|
|
||||||
shape: termwiz::surface::CursorShape::SteadyBlock,
|
|
||||||
visibility: termwiz::surface::CursorVisibility::Visible,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_seqno(&self) -> SequenceNo {
|
|
||||||
self.delegate.get_current_seqno()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_changed_since(
|
|
||||||
&self,
|
|
||||||
lines: Range<StableRowIndex>,
|
|
||||||
seqno: SequenceNo,
|
|
||||||
) -> RangeSet<StableRowIndex> {
|
|
||||||
let mut dirty = self.delegate.get_changed_since(lines.clone(), seqno);
|
|
||||||
dirty.add_set(&self.renderer.borrow().dirty_results);
|
|
||||||
dirty.intersection_with_range(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_lines(&self, lines: Range<StableRowIndex>) -> (StableRowIndex, Vec<Line>) {
|
|
||||||
let mut renderer = self.renderer.borrow_mut();
|
|
||||||
if self.delegate.get_current_seqno() > renderer.last_result_seqno {
|
|
||||||
renderer.update_search();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.check_for_resize();
|
|
||||||
let dims = self.get_dimensions();
|
|
||||||
|
|
||||||
let (top, mut lines) = self.delegate.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 = renderer.compute_search_row();
|
|
||||||
for (idx, line) in lines.iter_mut().enumerate() {
|
|
||||||
let stable_idx = idx as StableRowIndex + top;
|
|
||||||
renderer.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()), SEQ_ZERO);
|
|
||||||
let mode = &match renderer.pattern {
|
|
||||||
Pattern::CaseSensitiveString(_) => "case-sensitive",
|
|
||||||
Pattern::CaseInSensitiveString(_) => "ignore-case",
|
|
||||||
Pattern::Regex(_) => "regex",
|
|
||||||
};
|
|
||||||
line.overlay_text_with_attribute(
|
|
||||||
0,
|
|
||||||
&format!(
|
|
||||||
"Search: {} ({}/{} matches. {})",
|
|
||||||
*renderer.pattern,
|
|
||||||
renderer.result_pos.map(|x| x + 1).unwrap_or(0),
|
|
||||||
renderer.results.len(),
|
|
||||||
mode
|
|
||||||
),
|
|
||||||
rev,
|
|
||||||
SEQ_ZERO,
|
|
||||||
);
|
|
||||||
renderer.last_bar_pos = Some(search_row);
|
|
||||||
} else if let Some(matches) = renderer.by_line.get(&stable_idx) {
|
|
||||||
for m in matches {
|
|
||||||
// highlight
|
|
||||||
for cell_idx in m.range.clone() {
|
|
||||||
if let Some(cell) = line.cells_mut_for_attr_changes_only().get_mut(cell_idx)
|
|
||||||
{
|
|
||||||
if Some(m.result_index) == renderer.result_pos {
|
|
||||||
cell.attrs_mut()
|
|
||||||
.set_background(AnsiColor::Yellow)
|
|
||||||
.set_foreground(AnsiColor::Black)
|
|
||||||
.set_reverse(false);
|
|
||||||
} else {
|
|
||||||
cell.attrs_mut()
|
|
||||||
.set_background(AnsiColor::Fuchsia)
|
|
||||||
.set_foreground(AnsiColor::Black)
|
|
||||||
.set_reverse(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(top, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_dimensions(&self) -> RenderableDimensions {
|
|
||||||
self.delegate.get_dimensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchRenderable {
|
|
||||||
fn compute_search_row(&self) -> StableRowIndex {
|
|
||||||
let dims = self.delegate.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_for_pane(self.window.clone(), self.delegate.pane_id());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_viewport(&self, row: Option<StableRowIndex>) {
|
|
||||||
let dims = self.delegate.get_dimensions();
|
|
||||||
let pane_id = self.delegate.pane_id();
|
|
||||||
self.window
|
|
||||||
.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
|
||||||
term_window.set_viewport(pane_id, row, dims);
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_for_resize(&mut self) {
|
|
||||||
let dims = self.delegate.get_dimensions();
|
|
||||||
if dims.cols == self.width && dims.viewport_rows == self.height {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.width = dims.cols;
|
|
||||||
self.height = dims.viewport_rows;
|
|
||||||
|
|
||||||
let pos = self.result_pos;
|
|
||||||
self.update_search();
|
|
||||||
self.result_pos = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recompute_results(&mut self) {
|
|
||||||
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 if idx == res.start_y {
|
|
||||||
// first line of multi-line
|
|
||||||
res.start_x..self.width
|
|
||||||
} else {
|
|
||||||
// a middle line
|
|
||||||
0..self.width
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
self.last_result_seqno = self.delegate.get_current_seqno();
|
|
||||||
|
|
||||||
if !self.pattern.is_empty() {
|
|
||||||
let pane: Rc<dyn Pane> = self.delegate.clone();
|
|
||||||
let window = self.window.clone();
|
|
||||||
let pattern = self.pattern.clone();
|
|
||||||
promise::spawn::spawn(async move {
|
|
||||||
let mut results = pane.search(pattern).await?;
|
|
||||||
results.sort();
|
|
||||||
|
|
||||||
let pane_id = pane.pane_id();
|
|
||||||
let mut results = Some(results);
|
|
||||||
window.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
|
||||||
let state = term_window.pane_state(pane_id);
|
|
||||||
if let Some(overlay) = state.overlay.as_ref() {
|
|
||||||
if let Some(search_overlay) = overlay.pane.downcast_ref::<SearchOverlay>() {
|
|
||||||
let mut r = search_overlay.renderer.borrow_mut();
|
|
||||||
r.results = results.take().unwrap();
|
|
||||||
r.recompute_results();
|
|
||||||
let num_results = r.results.len();
|
|
||||||
|
|
||||||
if !r.results.is_empty() {
|
|
||||||
r.activate_match_number(num_results - 1);
|
|
||||||
} else {
|
|
||||||
r.set_viewport(None);
|
|
||||||
r.clear_selection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})));
|
|
||||||
anyhow::Result::<()>::Ok(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
} else {
|
|
||||||
self.set_viewport(None);
|
|
||||||
self.clear_selection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_selection(&mut self) {
|
|
||||||
let pane_id = self.delegate.pane_id();
|
|
||||||
self.window
|
|
||||||
.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
|
||||||
let mut selection = term_window.selection(pane_id);
|
|
||||||
selection.origin.take();
|
|
||||||
selection.range.take();
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activate_match_number(&mut self, n: usize) {
|
|
||||||
self.result_pos.replace(n);
|
|
||||||
let result = self.results[n].clone();
|
|
||||||
|
|
||||||
let pane_id = self.delegate.pane_id();
|
|
||||||
self.window
|
|
||||||
.notify(TermWindowNotif::Apply(Box::new(move |term_window| {
|
|
||||||
let mut selection = term_window.selection(pane_id);
|
|
||||||
let start = SelectionCoordinate {
|
|
||||||
x: result.start_x,
|
|
||||||
y: result.start_y,
|
|
||||||
};
|
|
||||||
selection.origin = Some(start);
|
|
||||||
selection.range = Some(SelectionRange {
|
|
||||||
start,
|
|
||||||
end: SelectionCoordinate {
|
|
||||||
// inclusive range for selection, but the result
|
|
||||||
// range is exclusive
|
|
||||||
x: result.end_x.saturating_sub(1),
|
|
||||||
y: result.end_y,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})));
|
|
||||||
|
|
||||||
self.set_viewport(Some(result.start_y));
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,8 +8,8 @@ use crate::glium::texture::SrgbTexture2d;
|
|||||||
use crate::inputmap::InputMap;
|
use crate::inputmap::InputMap;
|
||||||
use crate::overlay::{
|
use crate::overlay::{
|
||||||
confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program, launcher,
|
confirm_close_pane, confirm_close_tab, confirm_close_window, confirm_quit_program, launcher,
|
||||||
start_overlay, start_overlay_pane, CopyOverlay, LauncherArgs, LauncherFlags,
|
start_overlay, start_overlay_pane, CopyModeParams, CopyOverlay, LauncherArgs, LauncherFlags,
|
||||||
QuickSelectOverlay, SearchOverlay,
|
QuickSelectOverlay,
|
||||||
};
|
};
|
||||||
use crate::scripting::guiwin::GuiWin;
|
use crate::scripting::guiwin::GuiWin;
|
||||||
use crate::scripting::pane::PaneObject;
|
use crate::scripting::pane::PaneObject;
|
||||||
@ -22,7 +22,7 @@ use ::wezterm_term::input::{ClickPosition, MouseButton as TMB};
|
|||||||
use ::window::*;
|
use ::window::*;
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use config::keyassignment::{
|
use config::keyassignment::{
|
||||||
ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, QuickSelectArguments,
|
ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, Pattern, QuickSelectArguments,
|
||||||
SpawnCommand,
|
SpawnCommand,
|
||||||
};
|
};
|
||||||
use config::{
|
use config::{
|
||||||
@ -1405,8 +1405,7 @@ impl TermWindow {
|
|||||||
if dirty.is_empty() {
|
if dirty.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if pane.downcast_ref::<SearchOverlay>().is_none()
|
if pane.downcast_ref::<CopyOverlay>().is_none()
|
||||||
&& pane.downcast_ref::<CopyOverlay>().is_none()
|
|
||||||
&& pane.downcast_ref::<QuickSelectOverlay>().is_none()
|
&& pane.downcast_ref::<QuickSelectOverlay>().is_none()
|
||||||
{
|
{
|
||||||
// If any of the changed lines intersect with the
|
// If any of the changed lines intersect with the
|
||||||
@ -2252,9 +2251,29 @@ impl TermWindow {
|
|||||||
window.invalidate();
|
window.invalidate();
|
||||||
}
|
}
|
||||||
Search(pattern) => {
|
Search(pattern) => {
|
||||||
if let Some(pane) = self.get_active_pane_no_overlay() {
|
if let Some(pane) = self.get_active_pane_or_overlay() {
|
||||||
let search = SearchOverlay::with_pane(self, &pane, pattern.clone());
|
let mut replace_current = false;
|
||||||
self.assign_overlay_for_pane(pane.pane_id(), search);
|
if let Some(existing) = pane.downcast_ref::<CopyOverlay>() {
|
||||||
|
let mut params = existing.get_params();
|
||||||
|
params.editing_search = true;
|
||||||
|
if !pattern.is_empty() {
|
||||||
|
params.pattern = pattern.clone();
|
||||||
|
}
|
||||||
|
existing.apply_params(params);
|
||||||
|
replace_current = true;
|
||||||
|
} else {
|
||||||
|
let search = CopyOverlay::with_pane(
|
||||||
|
self,
|
||||||
|
&pane,
|
||||||
|
CopyModeParams {
|
||||||
|
pattern: pattern.clone(),
|
||||||
|
editing_search: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.assign_overlay_for_pane(pane.pane_id(), search);
|
||||||
|
}
|
||||||
|
self.key_table_state
|
||||||
|
.activate("search_mode", None, replace_current, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QuickSelect => {
|
QuickSelect => {
|
||||||
@ -2274,11 +2293,26 @@ impl TermWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActivateCopyMode => {
|
ActivateCopyMode => {
|
||||||
if let Some(pane) = self.get_active_pane_no_overlay() {
|
if let Some(pane) = self.get_active_pane_or_overlay() {
|
||||||
let copy = CopyOverlay::with_pane(self, &pane);
|
let mut replace_current = false;
|
||||||
self.assign_overlay_for_pane(pane.pane_id(), copy);
|
if let Some(existing) = pane.downcast_ref::<CopyOverlay>() {
|
||||||
|
let mut params = existing.get_params();
|
||||||
|
params.editing_search = false;
|
||||||
|
existing.apply_params(params);
|
||||||
|
replace_current = true;
|
||||||
|
} else {
|
||||||
|
let copy = CopyOverlay::with_pane(
|
||||||
|
self,
|
||||||
|
&pane,
|
||||||
|
CopyModeParams {
|
||||||
|
pattern: Pattern::default(),
|
||||||
|
editing_search: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.assign_overlay_for_pane(pane.pane_id(), copy);
|
||||||
|
}
|
||||||
self.key_table_state
|
self.key_table_state
|
||||||
.activate("copy_mode", None, false, false);
|
.activate("copy_mode", None, replace_current, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AdjustPaneSize(direction, amount) => {
|
AdjustPaneSize(direction, amount) => {
|
||||||
@ -2593,9 +2627,7 @@ impl TermWindow {
|
|||||||
// This is a bit gross. If we add other overlays that need this information,
|
// This is a bit gross. If we add other overlays that need this information,
|
||||||
// this should get extracted out into a trait
|
// this should get extracted out into a trait
|
||||||
if let Some(overlay) = state.overlay.as_ref() {
|
if let Some(overlay) = state.overlay.as_ref() {
|
||||||
if let Some(search_overlay) = overlay.pane.downcast_ref::<SearchOverlay>() {
|
if let Some(copy) = overlay.pane.downcast_ref::<CopyOverlay>() {
|
||||||
search_overlay.viewport_changed(pos);
|
|
||||||
} else if let Some(copy) = overlay.pane.downcast_ref::<CopyOverlay>() {
|
|
||||||
copy.viewport_changed(pos);
|
copy.viewport_changed(pos);
|
||||||
} else if let Some(qs) = overlay.pane.downcast_ref::<QuickSelectOverlay>() {
|
} else if let Some(qs) = overlay.pane.downcast_ref::<QuickSelectOverlay>() {
|
||||||
qs.viewport_changed(pos);
|
qs.viewport_changed(pos);
|
||||||
|
Loading…
Reference in New Issue
Block a user