1
1
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:
Wez Furlong 2022-05-05 07:36:32 -07:00
parent 865d857050
commit 2710cefb3e
6 changed files with 534 additions and 559 deletions

View File

@ -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);

View File

@ -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,

View File

@ -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),
), ),

View File

@ -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,

View File

@ -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));
}
}

View File

@ -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);