diff --git a/docs/changelog.md b/docs/changelog.md index a1f2b452e..7d7b8544a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -41,6 +41,7 @@ As features stabilize some brief notes about them will accumulate here. * [Key Table](config/key-tables.md) lookups will now keep searching the activation stack until a matching assignment is found, allowing for layered key tables. [#993](https://github.com/wez/wezterm/issues/993) * Search mode's search term is now remembered globally between activations of search mode. [#1912](https://github.com/wez/wezterm/issues/1912) * Quickselect no longer jumps to the bottom of the viewport when activated, allowing you to quickselect within the current viewport region +* Quickselect now supports multi-line anchors such as `^` and `$`. [#2008](https://github.com/wez/wezterm/issues/2008) #### Fixed * Flush after replying to XTGETTCAP and DECRQM. [#1850](https://github.com/wez/wezterm/issues/1850) [#1950](https://github.com/wez/wezterm/issues/1950) diff --git a/mux/src/localpane.rs b/mux/src/localpane.rs index 86e4e1b49..66415f238 100644 --- a/mux/src/localpane.rs +++ b/mux/src/localpane.rs @@ -538,6 +538,7 @@ impl Pane for LocalPane { Pattern::Regex(r) => { if let Ok(re) = regex::Regex::new(r) { // Allow for the regex to contain captures + log::trace!("regex search for {:?} in `{:?}`", r, haystack); for c in re.captures_iter(haystack) { // Look for the captures in reverse order, as index==0 is // the whole matched string. We can't just call @@ -545,6 +546,9 @@ impl Pane for LocalPane { for idx in (0..c.len()).rev() { if let Some(m) = c.get(idx) { let s = m.as_str(); + if s.is_empty() { + continue; + } let match_id = match uniq_matches.get(s).copied() { Some(id) => id, None => { @@ -577,6 +581,8 @@ impl Pane for LocalPane { let stable_row = screen.phys_to_stable_row_index(idx); let mut wrapped = false; + let mut trailing_spaces = None; + for (grapheme_idx, cell) in line.visible_cells() { coords.push(Coord { byte_idx: haystack.len(), @@ -585,6 +591,15 @@ impl Pane for LocalPane { }); let s = cell.str(); + if s == " " { + // Keep track of runs of trailing spaces; we'll prune + // them out so that `$` in a regex works as expected. + if trailing_spaces.is_none() { + trailing_spaces.replace(haystack.len()); + } + } else { + trailing_spaces.take(); + } if let Pattern::CaseInSensitiveString(_) = &pattern { // normalize the case so we match everything lowercase haystack.push_str(&s.to_lowercase()); @@ -594,6 +609,18 @@ impl Pane for LocalPane { wrapped = cell.attrs().wrapped(); } + if let Some(trailing_spaces) = trailing_spaces { + // Remove trailing spaces from the haystack + haystack.truncate(trailing_spaces); + while coords + .last() + .map(|c| c.byte_idx >= trailing_spaces) + .unwrap_or(false) + { + coords.pop(); + } + } + if !wrapped { if let Pattern::Regex(_) = &pattern { if let Some(coord) = coords.last().copied() { diff --git a/wezterm-gui/src/overlay/quickselect.rs b/wezterm-gui/src/overlay/quickselect.rs index fbf9fc790..db8667114 100644 --- a/wezterm-gui/src/overlay/quickselect.rs +++ b/wezterm-gui/src/overlay/quickselect.rs @@ -191,28 +191,32 @@ impl QuickSelectOverlay { let config = term_window.config.clone(); - let mut pattern = "(".to_string(); + let mut pattern = "(?m)(".to_string(); + let mut have_patterns = false; if !args.patterns.is_empty() { for p in &args.patterns { - if pattern.len() > 1 { + if have_patterns { pattern.push('|'); } pattern.push_str(p); + have_patterns = true; } } else { if !config.disable_default_quick_select_patterns { for p in &PATTERNS { - if pattern.len() > 1 { + if have_patterns { pattern.push('|'); } pattern.push_str(p); + have_patterns = true; } } for p in &config.quick_select_patterns { - if pattern.len() > 1 { + if have_patterns { pattern.push('|'); } pattern.push_str(p); + have_patterns = true; } } pattern.push(')');