diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-10 04:22:10 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-10 04:22:10 +0300 |
| commit | c8850a2e3e284033142db0f8117fa5998c68bd59 (patch) | |
| tree | 487530d94265c096ceeb3ded7c60a94f14a6f7fd /src/app/input | |
| parent | 0de503ccd9a8fedeeb6cbcc1446ab2bc644fdbb6 (diff) | |
feat: add copy mode search and tmux word motions
refs #1230
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/copy_mode.rs | 917 | ||||
| -rw-r--r-- | src/app/input/mod.rs | 18 |
2 files changed, 768 insertions, 167 deletions
diff --git a/src/app/input/copy_mode.rs b/src/app/input/copy_mode.rs index 6ced6409..9e6b3384 100644 --- a/src/app/input/copy_mode.rs +++ b/src/app/input/copy_mode.rs @@ -3,7 +3,7 @@ use unicode_width::UnicodeWidthChar; use crate::{ app::{ - state::{CopyModeSelection, CopyModeState}, + state::{CopyModeSearchDirection, CopyModeSearchPrompt, CopyModeSelection, CopyModeState}, App, AppState, Mode, }, input::TerminalKey, @@ -76,6 +76,10 @@ impl AppState { cursor_col: cursor.1.min(info.inner_rect.width.saturating_sub(1)), entry_offset_from_bottom, selection: None, + search: crate::app::state::CopyModeSearchState { + geometry: Some((info.inner_rect.width, info.inner_rect.height)), + ..Default::default() + }, }); self.mode = Mode::Copy; } @@ -85,8 +89,32 @@ impl AppState { terminal_runtimes: &TerminalRuntimeRegistry, key: TerminalKey, ) { + if self.handle_copy_mode_search_prompt_key(terminal_runtimes, key) { + return; + } match key.code { KeyCode::Esc => { + let should_clear = self.copy_mode.as_ref().is_some_and(|copy_mode| { + copy_mode.selection.is_some() + || !copy_mode.search.query.is_empty() + || !copy_mode.search.matches.is_empty() + || copy_mode.search.direction.is_some() + }); + if should_clear { + self.clear_copy_mode_selection(); + if let Some(search) = self + .copy_mode + .as_mut() + .map(|copy_mode| &mut copy_mode.search) + { + let geometry = search.geometry; + *search = crate::app::state::CopyModeSearchState { + geometry, + ..Default::default() + }; + } + return; + } self.exit_copy_mode(terminal_runtimes, false); return; } @@ -162,6 +190,10 @@ impl AppState { '0' => self.copy_mode_line_edge(terminal_runtimes, false), '$' => self.copy_mode_line_edge(terminal_runtimes, true), '^' => self.copy_mode_first_non_blank(terminal_runtimes), + '/' => self.open_copy_mode_search(CopyModeSearchDirection::Forward), + '?' => self.open_copy_mode_search(CopyModeSearchDirection::Backward), + 'n' => self.repeat_copy_mode_search(terminal_runtimes, false), + 'N' => self.repeat_copy_mode_search(terminal_runtimes, true), 'w' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextStart), 'b' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::PreviousStart), 'e' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextEnd), @@ -171,6 +203,185 @@ impl AppState { } } + fn handle_copy_mode_search_prompt_key( + &mut self, + terminal_runtimes: &TerminalRuntimeRegistry, + key: TerminalKey, + ) -> bool { + let Some(copy_mode) = self.copy_mode.as_mut() else { + return false; + }; + let Some(prompt) = copy_mode.search.prompt.as_mut() else { + return false; + }; + match key.code { + KeyCode::Esc => { + copy_mode.search.prompt = None; + } + KeyCode::Enter => { + let direction = prompt.direction; + let query = std::mem::take(&mut prompt.query); + copy_mode.search.prompt = None; + self.submit_copy_mode_search(terminal_runtimes, query, direction, false); + } + KeyCode::Backspace => { + prompt.query.pop(); + } + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + prompt.query.clear(); + } + _ => { + if let Some(ch) = copy_mode_command_char(key) { + prompt.query.push(ch); + } + } + } + true + } + + fn open_copy_mode_search(&mut self, direction: CopyModeSearchDirection) { + let Some(copy_mode) = self.copy_mode.as_mut() else { + return; + }; + copy_mode.search.prompt = Some(CopyModeSearchPrompt { + direction, + query: String::new(), + }); + } + + fn repeat_copy_mode_search( + &mut self, + terminal_runtimes: &TerminalRuntimeRegistry, + reverse: bool, + ) { + let Some(copy_mode) = self.copy_mode.as_ref() else { + return; + }; + if copy_mode.search.query.is_empty() { + return; + } + let Some(mut direction) = copy_mode.search.direction else { + return; + }; + if reverse { + direction = direction.reversed(); + } + self.submit_copy_mode_search( + terminal_runtimes, + copy_mode.search.query.clone(), + direction, + true, + ); + } + + fn submit_copy_mode_search( + &mut self, + terminal_runtimes: &TerminalRuntimeRegistry, + query: String, + direction: CopyModeSearchDirection, + repeat: bool, + ) { + if query.is_empty() { + return; + } + let Some(copy_mode) = self.copy_mode.as_ref() else { + return; + }; + let pane_id = copy_mode.pane_id; + let Some(ws_idx) = self.active else { + return; + }; + let Some(runtime) = self.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, pane_id) + else { + return; + }; + let Some(metrics) = runtime.scroll_metrics() else { + return; + }; + let cursor = crate::pane::TerminalTextPoint { + row: viewport_top_row(metrics).saturating_add(u32::from(copy_mode.cursor_row)), + col: copy_mode.cursor_col, + }; + let previous_match = repeat + .then(|| { + copy_mode + .search + .current + .and_then(|index| copy_mode.search.matches.get(index).copied()) + }) + .flatten() + .filter(|text_match| { + text_match.start == cursor && runtime.text_match_is_current(*text_match) + }); + let matches = runtime.search_text_matches(&query, query.chars().any(char::is_uppercase)); + let current = search_match_index(&matches, direction, cursor, previous_match); + + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.search.query = query; + if !repeat { + copy_mode.search.direction = Some(direction); + } + copy_mode.search.matches = matches; + copy_mode.search.current = current; + } + + let Some(target) = current.and_then(|index| { + self.copy_mode + .as_ref() + .and_then(|copy_mode| copy_mode.search.matches.get(index).copied()) + }) else { + return; + }; + self.move_copy_cursor_to_absolute(terminal_runtimes, target.start, true); + } + + fn move_copy_cursor_to_absolute( + &mut self, + terminal_runtimes: &TerminalRuntimeRegistry, + target: crate::pane::TerminalTextPoint, + reserve_overlay_row: bool, + ) { + let Some(copy_mode) = self.copy_mode.as_ref() else { + return; + }; + let pane_id = copy_mode.pane_id; + let Some(info) = self.pane_info_by_id(pane_id).cloned() else { + return; + }; + let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, pane_id) else { + return; + }; + let current_top = viewport_top_row(metrics); + let max_cursor_row = info + .inner_rect + .height + .saturating_sub(if reserve_overlay_row { 2 } else { 1 }); + let desired_top = if target.row < current_top { + target.row + } else if target.row > current_top.saturating_add(u32::from(max_cursor_row)) { + target.row.saturating_sub(u32::from(max_cursor_row)) + } else { + current_top + }; + let desired_offset = metrics + .max_offset_from_bottom + .saturating_sub(desired_top as usize); + self.set_pane_scroll_offset(terminal_runtimes, pane_id, desired_offset); + let Some(updated_metrics) = self.pane_scroll_metrics(terminal_runtimes, pane_id) else { + return; + }; + let updated_top = viewport_top_row(updated_metrics); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_row = target + .row + .saturating_sub(updated_top) + .min(u32::from(info.inner_rect.height.saturating_sub(1))) + as u16; + copy_mode.cursor_col = target.col.min(info.inner_rect.width.saturating_sub(1)); + } + self.sync_copy_mode_selection(terminal_runtimes); + } + pub(crate) fn cancel_copy_mode(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { self.exit_copy_mode(terminal_runtimes, false); } @@ -178,6 +389,7 @@ impl AppState { fn exit_copy_mode(&mut self, terminal_runtimes: &TerminalRuntimeRegistry, copy: bool) { let restore_scroll = self .copy_mode + .as_ref() .map(|copy_mode| (copy_mode.pane_id, copy_mode.entry_offset_from_bottom)); if copy { self.copy_selection(terminal_runtimes); @@ -196,7 +408,7 @@ impl AppState { } fn begin_copy_mode_selection(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; let Some(info) = self.pane_info_by_id(copy_mode.pane_id).cloned() else { @@ -221,23 +433,23 @@ impl AppState { } fn select_copy_mode_line(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id) else { + let pane_id = copy_mode.pane_id; + let cursor_row = copy_mode.cursor_row; + let Some(info) = self.pane_info_by_id(pane_id) else { return; }; let end_col = info.inner_rect.width.saturating_sub(1); - let metrics = self.pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id); - let anchor_row = Selection::absolute_row_for_viewport(copy_mode.cursor_row, metrics); + let metrics = self.pane_scroll_metrics(terminal_runtimes, pane_id); + let anchor_row = Selection::absolute_row_for_viewport(cursor_row, metrics); self.selection = Some(Selection::line_range( - copy_mode.pane_id, - anchor_row, - anchor_row, - end_col, + pane_id, anchor_row, anchor_row, end_col, )); - copy_mode.selection = Some(CopyModeSelection::Linewise { anchor_row }); - self.copy_mode = Some(copy_mode); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.selection = Some(CopyModeSelection::Linewise { anchor_row }); + } } fn move_copy_cursor( @@ -246,45 +458,48 @@ impl AppState { row_delta: i16, col_delta: i16, ) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id).cloned() else { + let pane_id = copy_mode.pane_id; + let mut cursor_row = copy_mode.cursor_row; + let mut cursor_col = copy_mode.cursor_col; + let Some(info) = self.pane_info_by_id(pane_id).cloned() else { self.exit_copy_mode(terminal_runtimes, false); return; }; if col_delta < 0 { - copy_mode.cursor_col = copy_mode - .cursor_col - .saturating_sub(col_delta.unsigned_abs()); + cursor_col = cursor_col.saturating_sub(col_delta.unsigned_abs()); } else if col_delta > 0 { - copy_mode.cursor_col = copy_mode - .cursor_col + cursor_col = cursor_col .saturating_add(col_delta as u16) .min(info.inner_rect.width.saturating_sub(1)); } if row_delta < 0 { let delta = row_delta.unsigned_abs(); - if copy_mode.cursor_row >= delta { - copy_mode.cursor_row -= delta; + if cursor_row >= delta { + cursor_row -= delta; } else { - self.scroll_pane_up(terminal_runtimes, copy_mode.pane_id, usize::from(delta)); - copy_mode.cursor_row = 0; + self.scroll_pane_up(terminal_runtimes, pane_id, usize::from(delta)); + cursor_row = 0; } } else if row_delta > 0 { let delta = row_delta as u16; let bottom = info.inner_rect.height.saturating_sub(1); - if copy_mode.cursor_row.saturating_add(delta) <= bottom { - copy_mode.cursor_row += delta; + if cursor_row.saturating_add(delta) <= bottom { + cursor_row += delta; } else { - self.scroll_pane_down(terminal_runtimes, copy_mode.pane_id, usize::from(delta)); - copy_mode.cursor_row = bottom; + self.scroll_pane_down(terminal_runtimes, pane_id, usize::from(delta)); + cursor_row = bottom; } } - self.copy_mode = Some(copy_mode); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_row = cursor_row; + copy_mode.cursor_col = cursor_col; + } self.sync_copy_mode_selection(terminal_runtimes); } @@ -294,15 +509,17 @@ impl AppState { direction: i16, half_page: bool, ) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id).cloned() else { + let pane_id = copy_mode.pane_id; + let mut cursor_row = copy_mode.cursor_row; + let Some(info) = self.pane_info_by_id(pane_id).cloned() else { self.exit_copy_mode(terminal_runtimes, false); return; }; let lines = copy_mode_page_lines(info.inner_rect.height, half_page); - if let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id) { + if let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, pane_id) { if direction < 0 { let next_offset = metrics.offset_from_bottom.saturating_add(lines); if next_offset > metrics.max_offset_from_bottom { @@ -312,96 +529,101 @@ impl AppState { let cursor_lines = lines.saturating_sub(scrolled_lines); self.set_pane_scroll_offset( terminal_runtimes, - copy_mode.pane_id, + pane_id, metrics.max_offset_from_bottom, ); - copy_mode.cursor_row = copy_mode - .cursor_row - .saturating_sub(cursor_lines.min(u16::MAX as usize) as u16); + cursor_row = + cursor_row.saturating_sub(cursor_lines.min(u16::MAX as usize) as u16); } else { - self.set_pane_scroll_offset(terminal_runtimes, copy_mode.pane_id, next_offset); + self.set_pane_scroll_offset(terminal_runtimes, pane_id, next_offset); } } else if metrics.offset_from_bottom < lines { let cursor_lines = lines.saturating_sub(metrics.offset_from_bottom); - self.set_pane_scroll_offset(terminal_runtimes, copy_mode.pane_id, 0); - copy_mode.cursor_row = copy_mode - .cursor_row + self.set_pane_scroll_offset(terminal_runtimes, pane_id, 0); + cursor_row = cursor_row .saturating_add(cursor_lines.min(u16::MAX as usize) as u16) .min(info.inner_rect.height.saturating_sub(1)); } else { self.set_pane_scroll_offset( terminal_runtimes, - copy_mode.pane_id, + pane_id, metrics.offset_from_bottom - lines, ); } } else if direction < 0 { - self.scroll_pane_up(terminal_runtimes, copy_mode.pane_id, lines); + self.scroll_pane_up(terminal_runtimes, pane_id, lines); } else { - self.scroll_pane_down(terminal_runtimes, copy_mode.pane_id, lines); + self.scroll_pane_down(terminal_runtimes, pane_id, lines); + } + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_row = cursor_row; } - self.copy_mode = Some(copy_mode); self.sync_copy_mode_selection(terminal_runtimes); } fn copy_mode_history_top(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id) else { + let pane_id = copy_mode.pane_id; + let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, pane_id) else { return; }; - self.set_pane_scroll_offset( - terminal_runtimes, - copy_mode.pane_id, - metrics.max_offset_from_bottom, - ); - copy_mode.cursor_row = 0; - self.copy_mode = Some(copy_mode); + self.set_pane_scroll_offset(terminal_runtimes, pane_id, metrics.max_offset_from_bottom); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + } self.sync_copy_mode_selection(terminal_runtimes); } fn copy_mode_history_bottom(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id) else { + let pane_id = copy_mode.pane_id; + let Some(info) = self.pane_info_by_id(pane_id) else { self.exit_copy_mode(terminal_runtimes, false); return; }; - self.set_pane_scroll_offset(terminal_runtimes, copy_mode.pane_id, 0); - copy_mode.cursor_row = info.inner_rect.height.saturating_sub(1); - self.copy_mode = Some(copy_mode); + let cursor_row = info.inner_rect.height.saturating_sub(1); + self.set_pane_scroll_offset(terminal_runtimes, pane_id, 0); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_row = cursor_row; + } self.sync_copy_mode_selection(terminal_runtimes); } fn copy_mode_line_edge(&mut self, terminal_runtimes: &TerminalRuntimeRegistry, end: bool) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id) else { + let pane_id = copy_mode.pane_id; + let Some(info) = self.pane_info_by_id(pane_id) else { self.exit_copy_mode(terminal_runtimes, false); return; }; - copy_mode.cursor_col = if end { + let cursor_col = if end { info.inner_rect.width.saturating_sub(1) } else { 0 }; - self.copy_mode = Some(copy_mode); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_col = cursor_col; + } self.sync_copy_mode_selection(terminal_runtimes); } fn copy_mode_first_non_blank(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(text) = self.copy_mode_visible_row_text(terminal_runtimes, copy_mode.cursor_row) - else { + let cursor_row = copy_mode.cursor_row; + let Some(text) = self.copy_mode_visible_row_text(terminal_runtimes, cursor_row) else { return; }; - copy_mode.cursor_col = first_non_blank_col(&text).unwrap_or(0); - self.copy_mode = Some(copy_mode); + if let Some(copy_mode) = self.copy_mode.as_mut() { + copy_mode.cursor_col = first_non_blank_col(&text).unwrap_or(0); + } self.sync_copy_mode_selection(terminal_runtimes); } @@ -410,51 +632,67 @@ impl AppState { terminal_runtimes: &TerminalRuntimeRegistry, motion: WordMotion, ) { - let Some(mut copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; - let Some(info) = self.pane_info_by_id(copy_mode.pane_id) else { - self.exit_copy_mode(terminal_runtimes, false); + let Some(metrics) = self.pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id) else { return; }; - let Some(text) = self.copy_mode_visible_row_text(terminal_runtimes, copy_mode.cursor_row) + let Some(ws_idx) = self.active else { + return; + }; + let Some(runtime) = + self.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, copy_mode.pane_id) else { return; }; - let Some(col) = word_motion_target(&text, copy_mode.cursor_col, motion) else { + let absolute_row = + viewport_top_row(metrics).saturating_add(u32::from(copy_mode.cursor_row)); + let motion = match motion { + WordMotion::NextStart => crate::pane::TerminalWordMotion::NextStart, + WordMotion::PreviousStart => crate::pane::TerminalWordMotion::PreviousStart, + WordMotion::NextEnd => crate::pane::TerminalWordMotion::NextEnd, + }; + let Some(target) = runtime.word_motion_target(absolute_row, copy_mode.cursor_col, motion) + else { return; }; - copy_mode.cursor_col = col.min(info.inner_rect.width.saturating_sub(1)); - self.copy_mode = Some(copy_mode); - self.sync_copy_mode_selection(terminal_runtimes); + self.move_copy_cursor_to_absolute(terminal_runtimes, target, false); } fn copy_mode_paragraph(&mut self, terminal_runtimes: &TerminalRuntimeRegistry, direction: i16) { - let Some(copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; + let pane_id = copy_mode.pane_id; let Some(pane_height) = self - .pane_info_by_id(copy_mode.pane_id) + .pane_info_by_id(pane_id) .map(|info| info.inner_rect.height) else { self.exit_copy_mode(terminal_runtimes, false); return; }; let limit = self - .pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id) + .pane_scroll_metrics(terminal_runtimes, pane_id) .map(|metrics| metrics.max_offset_from_bottom + metrics.viewport_rows) .unwrap_or(pane_height as usize) .clamp(1, 1000); for _ in 0..limit { - let before = self.copy_mode; + let before = self.copy_mode.as_ref().map(|copy_mode| { + ( + copy_mode.cursor_row, + copy_mode.cursor_col, + copy_mode.selection, + ) + }); let before_offset = self - .pane_scroll_metrics(terminal_runtimes, copy_mode.pane_id) + .pane_scroll_metrics(terminal_runtimes, pane_id) .map(|metrics| metrics.offset_from_bottom); self.move_copy_cursor(terminal_runtimes, direction, 0); - let Some(after) = self.copy_mode else { + let Some(after) = self.copy_mode.as_ref() else { return; }; if self @@ -468,8 +706,15 @@ impl AppState { else { continue; }; - let did_not_move = - before == self.copy_mode && before_offset == Some(after_metrics.offset_from_bottom); + let did_not_move = before + == self.copy_mode.as_ref().map(|copy_mode| { + ( + copy_mode.cursor_row, + copy_mode.cursor_col, + copy_mode.selection, + ) + }) + && before_offset == Some(after_metrics.offset_from_bottom); let at_top = direction < 0 && after.cursor_row == 0 && after_metrics.offset_from_bottom == after_metrics.max_offset_from_bottom; @@ -487,7 +732,7 @@ impl AppState { terminal_runtimes: &TerminalRuntimeRegistry, viewport_row: u16, ) -> Option<String> { - let copy_mode = self.copy_mode?; + let copy_mode = self.copy_mode.as_ref()?; let ws_idx = self.active?; let info = self.pane_info_by_id(copy_mode.pane_id)?; if viewport_row >= info.inner_rect.height || info.inner_rect.width == 0 { @@ -506,7 +751,7 @@ impl AppState { } pub(crate) fn copy_mode_pane_is_focused(&self) -> bool { - let Some(copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return false; }; self.current_pane_focus_target() @@ -539,6 +784,26 @@ impl AppState { self.sync_copy_mode_with_focus(); } + pub(crate) fn sync_copy_mode_search_geometry(&mut self) { + let geometry = self.copy_mode.as_ref().and_then(|copy_mode| { + self.view + .pane_infos + .iter() + .find(|info| info.id == copy_mode.pane_id) + .map(|info| (info.inner_rect.width, info.inner_rect.height)) + }); + let Some(copy_mode) = self.copy_mode.as_mut() else { + return; + }; + if let Some(geometry) = geometry { + if copy_mode.search.geometry.is_some() && copy_mode.search.geometry != Some(geometry) { + copy_mode.search.matches.clear(); + copy_mode.search.current = None; + } + copy_mode.search.geometry = Some(geometry); + } + } + pub(crate) fn clear_copy_mode_selection(&mut self) { self.clear_selection(); if let Some(copy_mode) = self.copy_mode.as_mut() { @@ -550,7 +815,7 @@ impl AppState { &mut self, pane_ids: impl IntoIterator<Item = crate::layout::PaneId>, ) { - let Some(copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; if pane_ids @@ -570,7 +835,7 @@ impl AppState { } fn sync_copy_mode_selection(&mut self, terminal_runtimes: &TerminalRuntimeRegistry) { - let Some(copy_mode) = self.copy_mode else { + let Some(copy_mode) = self.copy_mode.as_ref() else { return; }; let Some(selection) = copy_mode.selection else { @@ -605,6 +870,49 @@ impl AppState { } } +impl CopyModeSearchDirection { + fn reversed(self) -> Self { + match self { + Self::Forward => Self::Backward, + Self::Backward => Self::Forward, + } + } +} + +fn viewport_top_row(metrics: crate::pane::ScrollMetrics) -> u32 { + metrics + .max_offset_from_bottom + .saturating_sub(metrics.offset_from_bottom) + .min(u32::MAX as usize) as u32 +} + +fn search_match_index( + matches: &[crate::pane::TerminalTextMatch], + direction: CopyModeSearchDirection, + cursor: crate::pane::TerminalTextPoint, + previous: Option<crate::pane::TerminalTextMatch>, +) -> Option<usize> { + if matches.is_empty() { + return None; + } + match direction { + CopyModeSearchDirection::Forward => { + let origin = previous.map_or(cursor, |text_match| text_match.end); + matches + .iter() + .position(|text_match| text_match.start > origin) + .or(Some(0)) + } + CopyModeSearchDirection::Backward => { + let origin = previous.map_or(cursor, |text_match| text_match.start); + matches + .iter() + .rposition(|text_match| text_match.end < origin) + .or_else(|| matches.len().checked_sub(1)) + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum WordMotion { NextStart, @@ -612,12 +920,6 @@ enum WordMotion { NextEnd, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct WordSpan { - start: u16, - end: u16, -} - fn first_non_blank_col(text: &str) -> Option<u16> { let mut col = 0u16; for ch in text.chars() { @@ -629,62 +931,6 @@ fn first_non_blank_col(text: &str) -> Option<u16> { None } -fn word_motion_target(text: &str, cursor_col: u16, motion: WordMotion) -> Option<u16> { - let spans = word_spans(text); - match motion { - WordMotion::NextStart => spans.iter().enumerate().find_map(|(idx, span)| { - if cursor_col < span.start { - Some(span.start) - } else if cursor_col >= span.start && cursor_col <= span.end { - spans.get(idx + 1).map(|next| next.start) - } else { - None - } - }), - WordMotion::PreviousStart => spans - .iter() - .rev() - .find(|span| span.start < cursor_col) - .map(|span| span.start), - WordMotion::NextEnd => spans.iter().find_map(|span| { - if cursor_col < span.end { - Some(span.end) - } else { - None - } - }), - } -} - -fn word_spans(text: &str) -> Vec<WordSpan> { - let mut spans = Vec::new(); - let mut col = 0u16; - let mut start = None; - - for ch in text.chars() { - let width = char_cell_width(ch); - if ch.is_whitespace() { - if let Some(start_col) = start.take() { - spans.push(WordSpan { - start: start_col, - end: col.saturating_sub(1), - }); - } - } else if start.is_none() { - start = Some(col); - } - col = col.saturating_add(width); - } - - if let Some(start_col) = start { - spans.push(WordSpan { - start: start_col, - end: col.saturating_sub(1), - }); - } - spans -} - fn char_cell_width(ch: char) -> u16 { UnicodeWidthChar::width(ch).unwrap_or(1).max(1) as u16 } @@ -904,7 +1150,10 @@ mod tests { let (mut app, pane_id) = app_with_copy_screen(b"alpha\nbeta\n"); app.state.enter_copy_mode(&app.terminal_runtimes); assert_eq!(app.state.mode, Mode::Copy); - assert_eq!(app.state.copy_mode.expect("copy mode").pane_id, pane_id); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").pane_id, + pane_id + ); } #[tokio::test] @@ -914,7 +1163,7 @@ mod tests { app.state.prefix_code = KeyCode::Char('a'); app.state.prefix_mods = KeyModifiers::CONTROL; app.state.enter_copy_mode(&app.terminal_runtimes); - let height = app.state.copy_mode.expect("copy mode").cursor_row + 1; + let height = app.state.copy_mode.as_ref().expect("copy mode").cursor_row + 1; let expected_lines = copy_mode_page_lines(height, false); app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('b'), KeyModifiers::CONTROL)); @@ -944,7 +1193,7 @@ mod tests { async fn copy_mode_prefix_escape_returns_to_copy_mode() { let (mut app, _) = app_with_copy_screen(b"alpha\nbeta\n"); app.state.enter_copy_mode(&app.terminal_runtimes); - let copy_mode = app.state.copy_mode.expect("copy mode"); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode").clone(); app.handle_key(TerminalKey::new( app.state.prefix_code, @@ -962,7 +1211,7 @@ mod tests { async fn copy_mode_prefix_focus_keeps_copy_mode_on_source_pane() { let (mut app, first_pane, second_pane) = app_with_split_copy_screen(b"alpha\nbeta\n"); app.state.enter_copy_mode(&app.terminal_runtimes); - let copy_mode = app.state.copy_mode.expect("copy mode"); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode").clone(); assert_eq!(copy_mode.pane_id, first_pane); app.handle_key(TerminalKey::new( @@ -974,7 +1223,7 @@ mod tests { .await; assert_eq!(app.state.mode, Mode::Terminal); - assert_eq!(app.state.copy_mode, Some(copy_mode)); + assert_eq!(app.state.copy_mode, Some(copy_mode.clone())); assert_eq!( app.state.workspaces[0].tabs[0].layout.focused(), second_pane @@ -1054,7 +1303,10 @@ mod tests { crate::terminal::TerminalState::new(survivor_terminal, "/tmp".into()), ); app.state.enter_copy_mode(&app.terminal_runtimes); - assert_eq!(app.state.copy_mode.expect("copy mode").pane_id, first_pane); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").pane_id, + first_pane + ); app.handle_key(TerminalKey::new( app.state.prefix_code, @@ -1078,7 +1330,7 @@ mod tests { let bytes = numbered_lines_bytes(64); let (mut app, pane_id) = app_with_copy_scrollback(&bytes); app.state.enter_copy_mode(&app.terminal_runtimes); - let height = app.state.copy_mode.expect("copy mode").cursor_row + 1; + let height = app.state.copy_mode.as_ref().expect("copy mode").cursor_row + 1; let page_lines = copy_mode_page_lines(height, false); app.state .set_pane_scroll_offset(&app.terminal_runtimes, pane_id, page_lines); @@ -1099,13 +1351,332 @@ mod tests { } app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty())); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_col, 4); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 4 + ); app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('e'), KeyModifiers::empty())); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_col, 6); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 6 + ); app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('b'), KeyModifiers::empty())); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_col, 4); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 4 + ); + } + + fn submit_copy_search(app: &mut App, marker: char, query: &str) { + app.handle_copy_mode_key(TerminalKey::new( + KeyCode::Char(marker), + KeyModifiers::empty(), + )); + for ch in query.chars() { + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char(ch), KeyModifiers::empty())); + } + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Enter, KeyModifiers::empty())); + } + + #[tokio::test] + async fn copy_mode_search_wraps_and_repeats_in_both_directions() { + let (mut app, pane_id) = app_with_copy_screen(b"alpha needle\r\nbeta needle\r\ngamma\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + + submit_copy_search(&mut app, '/', "needle"); + + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!(copy_mode.search.matches.len(), 2); + assert_eq!(copy_mode.search.current, Some(0)); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 0 + ); + assert_eq!(copy_mode.cursor_col, 6); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('n'), KeyModifiers::empty())); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!(copy_mode.search.current, Some(1)); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 1 + ); + assert_eq!(copy_mode.cursor_col, 5); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('N'), KeyModifiers::SHIFT)); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!(copy_mode.search.current, Some(0)); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 0 + ); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('n'), KeyModifiers::empty())); + assert_eq!( + app.state + .copy_mode + .as_ref() + .expect("copy mode") + .search + .current, + Some(1) + ); + } + + #[tokio::test] + async fn copy_mode_backward_search_uses_last_match_before_cursor() { + let (mut app, pane_id) = app_with_copy_screen(b"alpha needle\r\nbeta needle\r\ngamma\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + + submit_copy_search(&mut app, '?', "needle"); + + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!(copy_mode.search.current, Some(1)); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 1 + ); + } + + #[tokio::test] + async fn copy_mode_shifted_slash_opens_backward_search() { + let (mut app, _) = app_with_copy_screen(b"alpha needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + + app.handle_copy_mode_key( + TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT) + .with_shifted_codepoint('?' as u32), + ); + + assert_eq!( + app.state + .copy_mode + .as_ref() + .and_then(|copy_mode| copy_mode.search.prompt.as_ref()) + .map(|prompt| prompt.direction), + Some(CopyModeSearchDirection::Backward) + ); + } + + #[tokio::test] + async fn copy_mode_search_extends_an_active_selection() { + let (mut app, _) = app_with_copy_screen(b"alpha\r\nbeta needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + if let Some(copy_mode) = app.state.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + copy_mode.cursor_col = 0; + } + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('v'), KeyModifiers::empty())); + + submit_copy_search(&mut app, '/', "needle"); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('y'), KeyModifiers::empty())); + + assert_eq!(copy_mode_clipboard_text(&mut app), "alpha\nbeta n"); + } + + #[tokio::test] + async fn copy_mode_search_prompt_escape_and_failure_preserve_copy_mode() { + let (mut app, pane_id) = app_with_copy_screen(b"alpha\r\nbeta\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + let before = ( + copy_mode_viewport_top_row(&app, pane_id), + app.state.copy_mode.as_ref().expect("copy mode").cursor_row, + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + ); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty())); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('x'), KeyModifiers::empty())); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + assert_eq!(app.state.mode, Mode::Copy); + assert!(app + .state + .copy_mode + .as_ref() + .expect("copy mode") + .search + .prompt + .is_none()); + + submit_copy_search(&mut app, '/', "missing"); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!(copy_mode.search.query, "missing"); + assert!(copy_mode.search.matches.is_empty()); + assert_eq!( + ( + copy_mode_viewport_top_row(&app, pane_id), + copy_mode.cursor_row, + copy_mode.cursor_col, + ), + before + ); + } + + #[tokio::test] + async fn copy_mode_escape_clears_search_before_exiting() { + let (mut app, _) = app_with_copy_screen(b"alpha needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + submit_copy_search(&mut app, '/', "needle"); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + + let search = &app.state.copy_mode.as_ref().expect("copy mode").search; + assert!(search.query.is_empty()); + assert!(search.matches.is_empty()); + assert!(search.current.is_none()); + assert_eq!(app.state.mode, Mode::Copy); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + assert_eq!(app.state.mode, Mode::Terminal); + assert!(app.state.copy_mode.is_none()); + } + + #[tokio::test] + async fn copy_mode_escape_clears_selection_before_exiting() { + let (mut app, _) = app_with_copy_screen(b"alpha\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('v'), KeyModifiers::empty())); + assert!(app.state.selection.is_some()); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + + assert_eq!(app.state.mode, Mode::Copy); + assert!(app.state.selection.is_none()); + assert!(app + .state + .copy_mode + .as_ref() + .expect("copy mode") + .selection + .is_none()); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[tokio::test] + async fn copy_mode_search_prompt_accepts_paste_without_exiting() { + let (mut app, _) = app_with_copy_screen(b"alpha needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty())); + + assert!(app.paste_into_active_text_input("needle")); + assert_eq!( + app.state + .copy_mode + .as_ref() + .and_then(|copy_mode| copy_mode.search.prompt.as_ref()) + .map(|prompt| prompt.query.as_str()), + Some("needle") + ); + assert_eq!(app.state.mode, Mode::Copy); + } + + #[tokio::test] + async fn copy_mode_word_motions_cross_rows_and_respect_separators() { + let (mut app, pane_id) = app_with_copy_screen(b"foo.bar_baz\r\nqux\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + if let Some(copy_mode) = app.state.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + copy_mode.cursor_col = 0; + } + + for expected_col in [3, 4] { + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty())); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + expected_col + ); + } + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty())); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 1 + ); + assert_eq!(copy_mode.cursor_col, 0); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('b'), KeyModifiers::empty())); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 0 + ); + assert_eq!(copy_mode.cursor_col, 4); + } + + #[tokio::test] + async fn copy_mode_search_does_not_change_live_follow_behavior() { + let bytes = numbered_lines_bytes(32); + let (mut app, pane_id) = app_with_copy_scrollback(&bytes); + app.state.enter_copy_mode(&app.terminal_runtimes); + submit_copy_search(&mut app, '/', "missing"); + assert_eq!(copy_mode_offset_from_bottom(&app, pane_id), 0); + + app.state + .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id) + .expect("runtime") + .test_process_pty_bytes(b"live output\r\n"); + assert_eq!(copy_mode_offset_from_bottom(&app, pane_id), 0); + + submit_copy_search(&mut app, '?', "000000"); + assert!(copy_mode_offset_from_bottom(&app, pane_id) > 0); + let runtime = app + .state + .runtime_for_pane_in_workspace(&app.terminal_runtimes, 0, pane_id) + .expect("runtime"); + let visible_before = runtime.visible_text(); + runtime.test_process_pty_bytes(b"more output\r\n"); + assert_eq!(runtime.visible_text(), visible_before); + } + + #[tokio::test] + async fn copy_mode_resize_clears_matches_but_keeps_query() { + let (mut app, _) = app_with_copy_screen(b"alpha needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + submit_copy_search(&mut app, '/', "needle"); + assert!(!app + .state + .copy_mode + .as_ref() + .expect("copy mode") + .search + .matches + .is_empty()); + + app.state.view.pane_infos[0].inner_rect.width = app.state.view.pane_infos[0] + .inner_rect + .width + .saturating_sub(1); + app.state.sync_copy_mode_search_geometry(); + + let search = &app.state.copy_mode.as_ref().expect("copy mode").search; + assert_eq!(search.query, "needle"); + assert!(search.matches.is_empty()); + assert!(search.current.is_none()); + } + + #[tokio::test] + async fn copy_mode_hidden_source_does_not_look_like_a_resize() { + let (mut app, _) = app_with_copy_screen(b"alpha needle\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + submit_copy_search(&mut app, '/', "needle"); + let before = app + .state + .copy_mode + .as_ref() + .expect("copy mode") + .search + .clone(); + + app.state.view.pane_infos.clear(); + app.state.sync_copy_mode_search_geometry(); + + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").search, + before + ); } #[tokio::test] @@ -1221,7 +1792,7 @@ mod tests { let bytes = numbered_lines_bytes(64); let (mut app, pane_id) = app_with_copy_scrollback(&bytes); app.state.enter_copy_mode(&app.terminal_runtimes); - let height = app.state.copy_mode.expect("copy mode").cursor_row + 1; + let height = app.state.copy_mode.as_ref().expect("copy mode").cursor_row + 1; let expected_lines = copy_mode_page_lines(height, false); app.handle_copy_mode_key(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())); @@ -1234,7 +1805,7 @@ mod tests { let bytes = numbered_lines_bytes(64); let (mut app, pane_id) = app_with_copy_scrollback(&bytes); app.state.enter_copy_mode(&app.terminal_runtimes); - let bottom = app.state.copy_mode.expect("copy mode").cursor_row; + let bottom = app.state.copy_mode.as_ref().expect("copy mode").cursor_row; let lines = copy_mode_page_lines(bottom + 1, true); let metrics = copy_mode_scroll_metrics(&app, pane_id); assert!(metrics.max_offset_from_bottom >= lines); @@ -1249,7 +1820,7 @@ mod tests { app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('u'), KeyModifiers::CONTROL)); - let copy_mode = app.state.copy_mode.expect("copy mode"); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); let expected_cursor_delta = 1; assert_eq!( copy_mode_offset_from_bottom(&app, pane_id), @@ -1266,7 +1837,7 @@ mod tests { let bytes = numbered_lines_bytes(64); let (mut app, pane_id) = app_with_copy_scrollback(&bytes); app.state.enter_copy_mode(&app.terminal_runtimes); - let bottom = app.state.copy_mode.expect("copy mode").cursor_row; + let bottom = app.state.copy_mode.as_ref().expect("copy mode").cursor_row; let lines = copy_mode_page_lines(bottom + 1, true); assert!(lines > 1); app.state @@ -1277,7 +1848,7 @@ mod tests { app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('d'), KeyModifiers::CONTROL)); - let copy_mode = app.state.copy_mode.expect("copy mode"); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); assert_eq!(copy_mode_offset_from_bottom(&app, pane_id), 0); assert_eq!(copy_mode.cursor_row, 1); } @@ -1328,19 +1899,31 @@ mod tests { } app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('6'), KeyModifiers::SHIFT)); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_col, 0); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 0 + ); app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char(']'), KeyModifiers::SHIFT)); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_row, 3); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_row, + 3 + ); app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('['), KeyModifiers::SHIFT)); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_row, 1); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_row, + 1 + ); app.handle_copy_mode_key( TerminalKey::new(KeyCode::Char(']'), KeyModifiers::SHIFT) .with_shifted_codepoint('}' as u32), ); - assert_eq!(app.state.copy_mode.expect("copy mode").cursor_row, 3); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_row, + 3 + ); } #[tokio::test] diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 450afab8..e4da8bc9 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -155,6 +155,20 @@ impl App { insert_navigator_search_text(&mut self.state, &self.terminal_runtimes, text); true } + Mode::Copy => { + let Some(prompt) = self + .state + .copy_mode + .as_mut() + .and_then(|copy_mode| copy_mode.search.prompt.as_mut()) + else { + return false; + }; + prompt + .query + .extend(text.chars().filter(|ch| !ch.is_control())); + true + } _ => false, } } @@ -513,6 +527,10 @@ pub(crate) fn modal_paste_target_active(state: &AppState) -> bool { .as_ref() .is_some_and(|open| open.search_focused), Mode::Navigator => state.navigator.search_focused, + Mode::Copy => state + .copy_mode + .as_ref() + .is_some_and(|copy_mode| copy_mode.search.prompt.is_some()), _ => false, } } |
