diff --git a/mux/src/pane.rs b/mux/src/pane.rs index dcea9ca250b..85d08144635 100644 --- a/mux/src/pane.rs +++ b/mux/src/pane.rs @@ -87,6 +87,23 @@ impl std::ops::DerefMut for Pattern { } } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum PatternType { + CaseSensitiveString, + CaseInSensitiveString, + Regex, +} + +impl From<&Pattern> for PatternType { + fn from(value: &Pattern) -> Self { + match value { + Pattern::CaseSensitiveString(_) => PatternType::CaseSensitiveString, + Pattern::CaseInSensitiveString(_) => PatternType::CaseInSensitiveString, + Pattern::Regex(_) => PatternType::Regex, + } + } +} + /// Why a close request is being made #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum CloseReason { diff --git a/termwiz/src/lineedit/buffer.rs b/termwiz/src/lineedit/buffer.rs new file mode 100644 index 00000000000..1d922d1a227 --- /dev/null +++ b/termwiz/src/lineedit/buffer.rs @@ -0,0 +1,189 @@ +use unicode_segmentation::GraphemeCursor; + +use super::actions::Movement; + +pub struct LineEditBuffer { + line: String, + /// byte index into the UTF-8 string data of the insertion + /// point. This is NOT the number of graphemes! + cursor: usize, +} + +impl Default for LineEditBuffer { + fn default() -> Self { + Self { + line: String::new(), + cursor: 0, + } + } +} + +impl LineEditBuffer { + pub fn new(line: &str, cursor: usize) -> Self { + let mut buffer = Self::default(); + buffer.set_line_and_cursor(line, cursor); + return buffer; + } + + pub fn get_line(&self) -> &str { + return &self.line; + } + + pub fn get_cursor(&self) -> usize { + return self.cursor; + } + + pub fn insert_char(&mut self, c: char) { + self.line.insert(self.cursor, c); + let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + self.cursor = pos; + } + } + + pub fn insert_text(&mut self, text: &str) { + self.line.insert_str(self.cursor, text); + self.cursor += text.len(); + } + + /// The cursor position is the byte index into the line UTF-8 bytes. + /// Panics: the cursor must be the first byte in a UTF-8 code point + /// sequence or the end of the provided line. + pub fn set_line_and_cursor(&mut self, line: &str, cursor: usize) { + assert!( + line.is_char_boundary(cursor), + "cursor {} is not a char boundary of the new line {}", + cursor, + line + ); + self.line = line.to_string(); + self.cursor = cursor; + } + + pub fn kill_text(&mut self, kill_movement: Movement, move_movement: Movement) { + let kill_pos = self.eval_movement(kill_movement); + let new_cursor = self.eval_movement(move_movement); + + let (lower, upper) = if kill_pos < self.cursor { + (kill_pos, self.cursor) + } else { + (self.cursor, kill_pos) + }; + + self.line.replace_range(lower..upper, ""); + + // Clamp to the line length, otherwise a kill to end of line + // command will leave the cursor way off beyond the end of + // the line. + self.cursor = new_cursor.min(self.line.len()); + } + + pub fn clear(&mut self) { + self.line.clear(); + self.cursor = 0; + } + + pub fn exec_movement(&mut self, movement: Movement) { + self.cursor = self.eval_movement(movement); + } + + /// Compute the cursor position after applying movement + fn eval_movement(&self, movement: Movement) -> usize { + match movement { + Movement::BackwardChar(rep) => { + let mut position = self.cursor; + for _ in 0..rep { + let mut cursor = GraphemeCursor::new(position, self.line.len(), false); + if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) { + position = pos; + } else { + break; + } + } + position + } + Movement::BackwardWord(rep) => { + let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); + if char_indices.is_empty() { + return self.cursor; + } + let mut char_position = char_indices + .iter() + .position(|(idx, _)| *idx == self.cursor) + .unwrap_or(char_indices.len() - 1); + + for _ in 0..rep { + if char_position == 0 { + break; + } + + let mut found = None; + for prev in (0..char_position - 1).rev() { + if char_indices[prev].1.is_whitespace() { + found = Some(prev + 1); + break; + } + } + + char_position = found.unwrap_or(0); + } + char_indices[char_position].0 + } + Movement::ForwardWord(rep) => { + let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); + if char_indices.is_empty() { + return self.cursor; + } + let mut char_position = char_indices + .iter() + .position(|(idx, _)| *idx == self.cursor) + .unwrap_or_else(|| char_indices.len()); + + for _ in 0..rep { + // Skip any non-whitespace characters + while char_position < char_indices.len() + && !char_indices[char_position].1.is_whitespace() + { + char_position += 1; + } + + // Skip any whitespace characters + while char_position < char_indices.len() + && char_indices[char_position].1.is_whitespace() + { + char_position += 1; + } + + // We are now on the start of the next word + } + char_indices + .get(char_position) + .map(|(i, _)| *i) + .unwrap_or_else(|| self.line.len()) + } + Movement::ForwardChar(rep) => { + let mut position = self.cursor; + for _ in 0..rep { + let mut cursor = GraphemeCursor::new(position, self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + position = pos; + } else { + break; + } + } + position + } + Movement::StartOfLine => 0, + Movement::EndOfLine => { + let mut cursor = + GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false); + if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { + pos + } else { + self.cursor + } + } + Movement::None => self.cursor, + } + } +} diff --git a/termwiz/src/lineedit/mod.rs b/termwiz/src/lineedit/mod.rs index 88a376db6e5..8130a051b61 100644 --- a/termwiz/src/lineedit/mod.rs +++ b/termwiz/src/lineedit/mod.rs @@ -43,12 +43,13 @@ use crate::surface::change::ChangeSequence; use crate::surface::{Change, Position}; use crate::terminal::{new_terminal, Terminal}; use crate::{bail, ensure, Result}; -use unicode_segmentation::GraphemeCursor; mod actions; +mod buffer; mod history; mod host; pub use actions::{Action, Movement, RepeatCount}; +pub use buffer::LineEditBuffer; pub use history::*; pub use host::*; @@ -71,10 +72,7 @@ pub use host::*; pub struct LineEditor<'term> { terminal: &'term mut dyn Terminal, prompt: String, - line: String, - /// byte index into the UTF-8 string data of the insertion - /// point. This is NOT the number of graphemes! - cursor: usize, + line: LineEditBuffer, history_pos: Option, bottom_line: Option, @@ -155,8 +153,7 @@ impl<'term> LineEditor<'term> { Self { terminal, prompt: "> ".to_owned(), - line: String::new(), - cursor: 0, + line: LineEditBuffer::default(), history_pos: None, bottom_line: None, completion: None, @@ -185,8 +182,8 @@ impl<'term> LineEditor<'term> { matching_line, cursor, .. - } => (matching_line, *cursor), - _ => (&self.line, self.cursor), + } => (matching_line.as_str(), *cursor), + _ => (self.line.get_line(), self.line.get_cursor()), }; let cursor_position_after_printing_prompt = changes.current_cursor_position(); @@ -258,7 +255,7 @@ impl<'term> LineEditor<'term> { // the text in the line editing area, but since the input // is drawn here, we render an `_` to indicate where the input // position really is. - changes.add(format!("\r\n{}: {}_", label, self.line)); + changes.add(format!("\r\n{}: {}_", label, self.line.get_line())); } // Add some debugging status at the bottom @@ -512,123 +509,9 @@ impl<'term> LineEditor<'term> { } } - /// Compute the cursor position after applying movement - fn eval_movement(&self, movement: Movement) -> usize { - match movement { - Movement::BackwardChar(rep) => { - let mut position = self.cursor; - for _ in 0..rep { - let mut cursor = GraphemeCursor::new(position, self.line.len(), false); - if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) { - position = pos; - } else { - break; - } - } - position - } - Movement::BackwardWord(rep) => { - let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); - if char_indices.is_empty() { - return self.cursor; - } - let mut char_position = char_indices - .iter() - .position(|(idx, _)| *idx == self.cursor) - .unwrap_or(char_indices.len() - 1); - - for _ in 0..rep { - if char_position == 0 { - break; - } - - let mut found = None; - for prev in (0..char_position - 1).rev() { - if char_indices[prev].1.is_whitespace() { - found = Some(prev + 1); - break; - } - } - - char_position = found.unwrap_or(0); - } - char_indices[char_position].0 - } - Movement::ForwardWord(rep) => { - let char_indices: Vec<(usize, char)> = self.line.char_indices().collect(); - if char_indices.is_empty() { - return self.cursor; - } - let mut char_position = char_indices - .iter() - .position(|(idx, _)| *idx == self.cursor) - .unwrap_or_else(|| char_indices.len()); - - for _ in 0..rep { - // Skip any non-whitespace characters - while char_position < char_indices.len() - && !char_indices[char_position].1.is_whitespace() - { - char_position += 1; - } - - // Skip any whitespace characters - while char_position < char_indices.len() - && char_indices[char_position].1.is_whitespace() - { - char_position += 1; - } - - // We are now on the start of the next word - } - char_indices - .get(char_position) - .map(|(i, _)| *i) - .unwrap_or_else(|| self.line.len()) - } - Movement::ForwardChar(rep) => { - let mut position = self.cursor; - for _ in 0..rep { - let mut cursor = GraphemeCursor::new(position, self.line.len(), false); - if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { - position = pos; - } else { - break; - } - } - position - } - Movement::StartOfLine => 0, - Movement::EndOfLine => { - let mut cursor = - GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false); - if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { - pos - } else { - self.cursor - } - } - Movement::None => self.cursor, - } - } - fn kill_text(&mut self, kill_movement: Movement, move_movement: Movement) { self.clear_completion(); - let kill_pos = self.eval_movement(kill_movement); - let new_cursor = self.eval_movement(move_movement); - - let (lower, upper) = if kill_pos < self.cursor { - (kill_pos, self.cursor) - } else { - (self.cursor, kill_pos) - }; - - self.line.replace_range(lower..upper, ""); - - // Clamp to the line length, otherwise a kill to end of line - // command will leave the cursor way off beyond the end of - // the line. - self.cursor = new_cursor.min(self.line.len()); + self.line.kill_text(kill_movement, move_movement); } fn clear_completion(&mut self) { @@ -642,8 +525,7 @@ impl<'term> LineEditor<'term> { .. } = &self.state { - self.line = matching_line.to_string(); - self.cursor = *cursor; + self.line.set_line_and_cursor(matching_line, *cursor); self.state = EditorState::Editing; } } @@ -653,23 +535,17 @@ impl<'term> LineEditor<'term> { /// a custom editor operation on the line buffer contents. /// The cursor position is the byte index into the line UTF-8 bytes. pub fn get_line_and_cursor(&mut self) -> (&str, usize) { - (&self.line, self.cursor) + (self.line.get_line(), self.line.get_cursor()) } /// Sets the current line and cursor position. /// You don't normally need to call this unless you are defining /// a custom editor operation on the line buffer contents. /// The cursor position is the byte index into the line UTF-8 bytes. - /// Panics: the cursor must be within the bounds of the provided line. + /// Panics: the cursor must be the first byte in a UTF-8 code point + /// sequence or the end of the provided line. pub fn set_line_and_cursor(&mut self, line: &str, cursor: usize) { - assert!( - cursor < line.len(), - "cursor {} is outside the byte length of the new line of length {}", - cursor, - line.len() - ); - self.line = line.to_string(); - self.cursor = cursor; + self.line.set_line_and_cursor(line, cursor); } /// Call this after changing modifying the line buffer. @@ -698,9 +574,9 @@ impl<'term> LineEditor<'term> { let last_matching_line; let last_cursor; - if let Some(result) = host - .history() - .search(history_pos, *style, *direction, &self.line) + if let Some(result) = + host.history() + .search(history_pos, *style, *direction, self.line.get_line()) { self.history_pos.replace(result.idx); last_matching_line = result.line.to_string(); @@ -733,7 +609,6 @@ impl<'term> LineEditor<'term> { // Not yet searching, so we start a new search // with an empty pattern self.line.clear(); - self.cursor = 0; self.history_pos.take(); } @@ -752,9 +627,9 @@ impl<'term> LineEditor<'term> { }, }; - let search_result = host - .history() - .search(history_pos, style, direction, &self.line); + let search_result = + host.history() + .search(history_pos, style, direction, self.line.get_line()); let last_matching_line; let last_cursor; @@ -836,25 +711,20 @@ impl<'term> LineEditor<'term> { Action::Move(movement) => { self.clear_completion(); self.cancel_search_state(); - self.cursor = self.eval_movement(movement); + self.line.exec_movement(movement); } Action::InsertChar(rep, c) => { self.clear_completion(); for _ in 0..rep { - self.line.insert(self.cursor, c); - let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); - if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { - self.cursor = pos; - } + self.line.insert_char(c); } self.reapply_search_pattern(host); } Action::InsertText(rep, text) => { self.clear_completion(); for _ in 0..rep { - self.line.insert_str(self.cursor, &text); - self.cursor += text.len(); + self.line.insert_text(&text); } self.reapply_search_pattern(host); } @@ -870,18 +740,16 @@ impl<'term> LineEditor<'term> { let prior_idx = cur_pos.saturating_sub(1); if let Some(prior) = host.history().get(prior_idx) { self.history_pos = Some(prior_idx); - self.line = prior.to_string(); - self.cursor = self.line.len(); + self.line.set_line_and_cursor(&prior, prior.len()); } } else if let Some(last) = host.history().last() { - self.bottom_line = Some(self.line.clone()); + self.bottom_line = Some(self.line.get_line().to_string()); self.history_pos = Some(last); - self.line = host + let line = host .history() .get(last) - .expect("History::last and History::get to be consistent") - .to_string(); - self.cursor = self.line.len(); + .expect("History::last and History::get to be consistent"); + self.line.set_line_and_cursor(&line, line.len()) } } Action::HistoryNext => { @@ -892,14 +760,11 @@ impl<'term> LineEditor<'term> { let next_idx = cur_pos.saturating_add(1); if let Some(next) = host.history().get(next_idx) { self.history_pos = Some(next_idx); - self.line = next.to_string(); - self.cursor = self.line.len(); + self.line.set_line_and_cursor(&next, next.len()); } else if let Some(bottom) = self.bottom_line.take() { - self.line = bottom; - self.cursor = self.line.len(); + self.line.set_line_and_cursor(&bottom, bottom.len()); } else { self.line.clear(); - self.cursor = 0; } } } @@ -915,18 +780,17 @@ impl<'term> LineEditor<'term> { self.cancel_search_state(); if self.completion.is_none() { - let candidates = host.complete(&self.line, self.cursor); + let candidates = host.complete(self.line.get_line(), self.line.get_cursor()); if !candidates.is_empty() { let state = CompletionState { candidates, index: 0, - original_line: self.line.clone(), - original_cursor: self.cursor, + original_line: self.line.get_line().to_string(), + original_cursor: self.line.get_cursor(), }; let (cursor, line) = state.current(); - self.cursor = cursor; - self.line = line; + self.line.set_line_and_cursor(&line, cursor); // If there is only a single completion then don't // leave us in a state where we just cycle on the @@ -938,8 +802,7 @@ impl<'term> LineEditor<'term> { } else if let Some(state) = self.completion.as_mut() { state.next(); let (cursor, line) = state.current(); - self.cursor = cursor; - self.line = line; + self.line.set_line_and_cursor(&line, cursor); } } } @@ -949,7 +812,6 @@ impl<'term> LineEditor<'term> { fn read_line_impl(&mut self, host: &mut dyn LineEditorHost) -> Result> { self.line.clear(); - self.cursor = 0; self.history_pos = None; self.bottom_line = None; self.clear_completion(); @@ -964,14 +826,14 @@ impl<'term> LineEditor<'term> { match self.state { EditorState::Searching { .. } | EditorState::Editing => {} EditorState::Cancelled => return Ok(None), - EditorState::Accepted => return Ok(Some(self.line.clone())), + EditorState::Accepted => return Ok(Some(self.line.get_line().to_string())), EditorState::Inactive => bail!("editor is inactive during read line!?"), } } else { self.render(host)?; } } - Ok(Some(self.line.clone())) + Ok(Some(self.line.get_line().to_string())) } } diff --git a/wezterm-gui/src/overlay/copy.rs b/wezterm-gui/src/overlay/copy.rs index c06b0018c14..e4de21b8c8e 100644 --- a/wezterm-gui/src/overlay/copy.rs +++ b/wezterm-gui/src/overlay/copy.rs @@ -7,7 +7,7 @@ use config::keyassignment::{ }; use mux::domain::DomainId; use mux::pane::{ - CachePolicy, ForEachPaneLogicalLine, LogicalLine, Pane, PaneId, Pattern, + CachePolicy, ForEachPaneLogicalLine, LogicalLine, Pane, PaneId, Pattern, PatternType, PerformAssignmentResult, SearchResult, WithPaneLines, }; use mux::renderable::*; @@ -21,6 +21,7 @@ use std::sync::Arc; use std::time::Duration; use termwiz::cell::{Cell, CellAttributes}; use termwiz::color::AnsiColor; +use termwiz::lineedit::{LineEditBuffer, Movement}; use termwiz::surface::{CursorVisibility, SequenceNo, SEQ_ZERO}; use unicode_segmentation::*; use url::Url; @@ -65,7 +66,8 @@ struct CopyRenderable { window: ::window::Window, /// The text that the user entered - pattern: Pattern, + pattern_type: PatternType, + search_line: LineEditBuffer, /// The most recently queried set of matches results: Vec, by_line: HashMap>, @@ -125,6 +127,17 @@ impl CopyOverlay { .clone() .ok_or_else(|| anyhow::anyhow!("failed to clone window handle"))?; let dims = pane.get_dimensions(); + let pattern = if params.pattern.is_empty() { + SAVED_PATTERN + .lock() + .get(&tab_id) + .map(|p| p.clone()) + .unwrap_or(params.pattern) + } else { + params.pattern + }; + let search_line = LineEditBuffer::new(&pattern, pattern.len()); + let mut render = CopyRenderable { cursor, window, @@ -139,15 +152,8 @@ impl CopyOverlay { last_result_seqno: SEQ_ZERO, last_bar_pos: None, tab_id, - pattern: if params.pattern.is_empty() { - SAVED_PATTERN - .lock() - .get(&tab_id) - .map(|p| p.clone()) - .unwrap_or(params.pattern) - } else { - params.pattern - }, + pattern_type: PatternType::from(&pattern), + search_line, editing_search: params.editing_search, result_pos: None, selection_mode: SelectionMode::Cell, @@ -170,7 +176,7 @@ impl CopyOverlay { pub fn get_params(&self) -> CopyModeParams { let render = self.render.lock(); CopyModeParams { - pattern: render.pattern.clone(), + pattern: render.get_pattern(), editing_search: render.editing_search, } } @@ -178,8 +184,11 @@ impl CopyOverlay { pub fn apply_params(&self, params: CopyModeParams) { let mut render = self.render.lock(); render.editing_search = params.editing_search; - if render.pattern != params.pattern { - render.pattern = params.pattern; + if render.get_pattern() != params.pattern { + render.pattern_type = PatternType::from(¶ms.pattern); + render + .search_line + .set_line_and_cursor(¶ms.pattern, params.pattern.len()); render.schedule_update_search(); } let search_row = render.compute_search_row(); @@ -293,18 +302,16 @@ impl CopyRenderable { self.by_line.clear(); self.result_pos.take(); - SAVED_PATTERN - .lock() - .insert(self.tab_id, self.pattern.clone()); + SAVED_PATTERN.lock().insert(self.tab_id, self.get_pattern()); 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 pattern = self.get_pattern(); + if !pattern.is_empty() { let pane: Arc = self.delegate.clone(); let window = self.window.clone(); - let pattern = self.pattern.clone(); let dims = pane.get_dimensions(); let end = dims.scrollback_top + dims.scrollback_rows as StableRowIndex; @@ -350,7 +357,7 @@ impl CopyRenderable { range: Range, ) { self.window.invalidate(); - if pattern != self.pattern { + if pattern != self.get_pattern() { return; } let is_first = self.results.is_empty(); @@ -627,8 +634,17 @@ impl CopyRenderable { } } + fn get_pattern(&self) -> Pattern { + let pattern = self.search_line.get_line().to_string(); + match self.pattern_type { + PatternType::CaseSensitiveString => Pattern::CaseSensitiveString(pattern), + PatternType::CaseInSensitiveString => Pattern::CaseInSensitiveString(pattern), + PatternType::Regex => Pattern::Regex(pattern), + } + } + fn clear_pattern(&mut self) { - self.pattern.clear(); + self.search_line.clear(); self.update_search(); } @@ -670,12 +686,12 @@ impl CopyRenderable { } 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()), + let pattern_type = match &self.pattern_type { + PatternType::CaseSensitiveString => PatternType::CaseInSensitiveString, + PatternType::CaseInSensitiveString => PatternType::Regex, + PatternType::Regex => PatternType::CaseSensitiveString, }; - self.pattern = pattern; + self.pattern_type = pattern_type; self.schedule_update_search(); } @@ -1090,7 +1106,7 @@ impl Pane for CopyOverlay { fn send_paste(&self, text: &str) -> anyhow::Result<()> { // paste into the search bar let mut r = self.render.lock(); - r.pattern.push_str(text); + r.search_line.insert_text(text); r.schedule_update_search(); Ok(()) } @@ -1141,14 +1157,71 @@ impl Pane for CopyOverlay { (KeyCode::Char(c), KeyModifiers::NONE) | (KeyCode::Char(c), KeyModifiers::SHIFT) => { // Type to add to the pattern - render.pattern.push(c); + render.search_line.insert_char(c); + render.schedule_update_search(); } - (KeyCode::Backspace, KeyModifiers::NONE) => { - // Backspace to edit the pattern - render.pattern.pop(); + (KeyCode::Char('H'), KeyModifiers::CTRL) + | (KeyCode::Backspace, KeyModifiers::NONE) => { + render + .search_line + .kill_text(Movement::BackwardChar(1), Movement::BackwardChar(1)); + render.schedule_update_search(); } + (KeyCode::Delete, KeyModifiers::NONE) => { + render + .search_line + .kill_text(Movement::ForwardChar(1), Movement::None); + + render.schedule_update_search(); + } + (KeyCode::Backspace, KeyModifiers::ALT) + | (KeyCode::Char('W'), KeyModifiers::CTRL) => { + render + .search_line + .kill_text(Movement::BackwardWord(1), Movement::BackwardWord(1)); + + render.schedule_update_search(); + } + (KeyCode::Backspace, KeyModifiers::SUPER) => { + render + .search_line + .kill_text(Movement::StartOfLine, Movement::StartOfLine); + + render.schedule_update_search(); + } + (KeyCode::Char('K'), KeyModifiers::CTRL) => { + render + .search_line + .kill_text(Movement::EndOfLine, Movement::EndOfLine); + + render.schedule_update_search(); + } + (KeyCode::Char('B'), KeyModifiers::CTRL) + | (KeyCode::ApplicationLeftArrow, KeyModifiers::NONE) + | (KeyCode::LeftArrow, KeyModifiers::NONE) => { + render.search_line.exec_movement(Movement::BackwardChar(1)); + } + (KeyCode::Char('F'), KeyModifiers::CTRL) + | (KeyCode::ApplicationRightArrow, KeyModifiers::NONE) + | (KeyCode::RightArrow, KeyModifiers::NONE) => { + render.search_line.exec_movement(Movement::ForwardChar(1)); + } + (KeyCode::ApplicationLeftArrow, KeyModifiers::CTRL) + | (KeyCode::LeftArrow, KeyModifiers::CTRL) => { + render.search_line.exec_movement(Movement::BackwardWord(1)); + } + (KeyCode::ApplicationRightArrow, KeyModifiers::CTRL) + | (KeyCode::RightArrow, KeyModifiers::CTRL) => { + render.search_line.exec_movement(Movement::ForwardWord(1)); + } + (KeyCode::Char('A'), KeyModifiers::CTRL) | (KeyCode::Home, KeyModifiers::NONE) => { + render.search_line.exec_movement(Movement::StartOfLine); + } + (KeyCode::Char('E'), KeyModifiers::CTRL) | (KeyCode::End, KeyModifiers::NONE) => { + render.search_line.exec_movement(Movement::EndOfLine); + } _ => {} } } @@ -1259,8 +1332,14 @@ impl Pane for CopyOverlay { let renderer = self.render.lock(); if renderer.editing_search { // place in the search box + // Padding between the start of the editable line and the left side of the terminal + const SEARCH_CURSOR_PADDING: usize = 8; + let cursor = unicode_column_width( + &renderer.search_line.get_line()[0..renderer.search_line.get_cursor()], + None, + ); StableCursorPosition { - x: 8 + wezterm_term::unicode_column_width(&renderer.pattern, None), + x: SEARCH_CURSOR_PADDING + cursor, y: renderer.compute_search_row(), shape: termwiz::surface::CursorShape::SteadyBlock, visibility: termwiz::surface::CursorVisibility::Visible, @@ -1335,13 +1414,14 @@ impl Pane for CopyOverlay { let stable_idx = idx as StableRowIndex + first_row; self.renderer.dirty_results.remove(stable_idx); + let pattern = self.renderer.get_pattern(); if stable_idx == self.search_row - && (self.renderer.editing_search || !self.renderer.pattern.is_empty()) + && (self.renderer.editing_search || !pattern.is_empty()) { // Replace with search UI let rev = CellAttributes::default().set_reverse(true).clone(); line.fill_range(0..self.dims.cols, &Cell::new(' ', rev.clone()), SEQ_ZERO); - let mode = &match self.renderer.pattern { + let mode = &match pattern { Pattern::CaseSensitiveString(_) => "case-sensitive", Pattern::CaseInSensitiveString(_) => "ignore-case", Pattern::Regex(_) => "regex", @@ -1358,7 +1438,7 @@ impl Pane for CopyOverlay { 0, &format!( "Search: {} ({}/{} matches. {}{remain})", - *self.renderer.pattern, + *pattern, self.renderer.result_pos.map(|x| x + 1).unwrap_or(0), self.renderer.results.len(), mode @@ -1437,12 +1517,12 @@ impl Pane for CopyOverlay { 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()) - { + let pattern = renderer.get_pattern(); + if stable_idx == search_row && (renderer.editing_search || !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 { + let mode = &match pattern { Pattern::CaseSensitiveString(_) => "case-sensitive", Pattern::CaseInSensitiveString(_) => "ignore-case", Pattern::Regex(_) => "regex", @@ -1451,7 +1531,7 @@ impl Pane for CopyOverlay { 0, &format!( "Search: {} ({}/{} matches. {})", - *renderer.pattern, + *pattern, renderer.result_pos.map(|x| x + 1).unwrap_or(0), renderer.results.len(), mode