Files
ghostty-rs/crates/ghostty-terminal/src/search/active.rs
T
ddidderr 8bcb85a29a style: apply nightly rustfmt import grouping across the workspace
rustfmt.toml sets group_imports, imports_granularity and imports_layout,
which are nightly-only; agents formatted with stable rustfmt, which
ignored them. This is a formatting-only pass with `cargo +nightly fmt`.

Test Plan:
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace
2026-10-12 05:03:37 +02:00

156 lines
5.3 KiB
Rust

//! Searches for a substring within the active area of a PageList.
//!
//! The distinction for "active area" is important because it is the
//! only part of a PageList that is mutable. Therefore, its the only part
//! of the terminal that needs to be repeatedly searched as the contents
//! change.
//!
//! This struct specializes in searching only within that active area,
//! and handling the active area moving as new lines are added to the bottom.
use super::sliding_window::{Direction, SlidingWindow};
use crate::{
highlight::Flattened,
page_list::{NodeId, PageList},
};
#[derive(Debug)]
pub struct ActiveSearch {
pub window: SlidingWindow,
}
impl ActiveSearch {
pub fn new(needle: &[u8]) -> Self {
// We just do a forward search since the active area is usually
// pretty small so search results are instant anyways. This avoids
// a small amount of work to reverse things.
Self {
window: SlidingWindow::new(Direction::Forward, needle),
}
}
/// Update the active area to reflect the current state of the PageList.
///
/// This doesn't do the search, it only copies the necessary data
/// to perform the search later. This lets the caller hold the lock
/// on the PageList for a minimal amount of time.
///
/// This returns the first page (in reverse order) covered by this
/// search. This allows the history search to overlap and search history.
/// There CAN BE duplicates, and this page CAN BE mutable, so the history
/// search results should prune anything that's in the active area.
///
/// If the return value is `None` it means the active area covers the entire
/// PageList, currently.
pub fn update(&mut self, list: &PageList) -> Option<NodeId> {
// Clear our previous sliding window
self.window.clear_and_retain_capacity();
// First up, add enough pages to cover the active area.
let mut rem: usize = usize::from(list.rows);
let mut node_ = list.last;
let mut last_node: Option<NodeId> = None;
while let Some(id) = node_ {
let Some(node) = list.node(id) else { break };
self.window.append(list, id);
last_node = Some(id);
// If we reached our target amount, then this is the last
// page that contains the active area. We go to the previous
// page once more since its the first page of our required
// overlap.
if rem <= usize::from(node.data.size.rows) {
node_ = node.prev;
break;
}
rem -= usize::from(node.data.size.rows);
node_ = node.prev;
}
// Next, add enough overlap to cover needle.len - 1 bytes (if it
// exists) so we can cover the overlap.
while let Some(id) = node_ {
let Some(node) = list.node(id) else { break };
// If the last row of this node isn't wrapped we can't overlap.
let row = node
.data
.get_row(usize::from(node.data.size.rows) - 1)
.expect("last row");
if !row.wrap() {
break;
}
// We could be more accurate here and count bytes since the
// last wrap but its complicated and unlikely multiple pages
// wrap so this should be fine.
let added = self.window.append(list, id);
if added >= self.window.needle().len() - 1 {
break;
}
node_ = node.prev;
}
// Return the last node we added to our window.
last_node
}
/// Find the next match for the needle in the active area. This returns
/// `None` when there are no more matches.
pub fn next(&mut self) -> Option<Flattened> {
self.window.next()
}
}
#[cfg(test)]
mod tests {
use super::{super::test_util::*, *};
use crate::point::Tag;
fn check(list: &PageList, h: &Flattened, s: (u16, u32), e: (u16, u32)) {
let sel = h.untracked();
assert_eq!(
list.point_from_pin(Tag::Active, sel.start),
Some(active(s.0, s.1))
);
assert_eq!(
list.point_from_pin(Tag::Active, sel.end),
Some(active(e.0, e.1))
);
}
#[test]
fn simple_search() {
let mut t = terminal(10, 10);
feed(&mut t, b"Fizz\r\nBuzz\r\nFizz\r\nBang");
let mut search = ActiveSearch::new(b"Fizz");
search.update(&t.screen().pages);
let h = search.next().unwrap();
check(&t.screen().pages, &h, (0, 0), (3, 0));
let h = search.next().unwrap();
check(&t.screen().pages, &h, (0, 2), (3, 2));
assert!(search.next().is_none());
}
#[test]
fn clear_screen_and_search() {
let mut t = terminal(10, 10);
feed(&mut t, b"Fizz\r\nBuzz\r\nFizz\r\nBang");
let mut search = ActiveSearch::new(b"Fizz");
search.update(&t.screen().pages);
feed(&mut t, b"\x1b[2J"); // Clear screen
feed(&mut t, b"\x1b[H"); // Move cursor home
feed(&mut t, b"Buzz\r\nFizz\r\nBuzz");
search.update(&t.screen().pages);
let h = search.next().unwrap();
check(&t.screen().pages, &h, (0, 1), (3, 1));
assert!(search.next().is_none());
}
}