Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
//! This module implements terminal find functionality for the alt screen.
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::{
|
||||
terminal::model::{
|
||||
alt_screen::AltScreen,
|
||||
find::{FindConfig, RegexDFAs},
|
||||
index::Point,
|
||||
},
|
||||
view_components::find::FindDirection,
|
||||
};
|
||||
|
||||
use super::FindOptions;
|
||||
|
||||
/// Runs a find operation on the blocklist using the given `options` and returns an
|
||||
/// `AltScreenFindRun` with the results.
|
||||
///
|
||||
/// If the given `options` does not contain a query, returns `None`.
|
||||
pub(super) fn run_find_on_alt_screen(
|
||||
options: FindOptions,
|
||||
alt_screen: &AltScreen,
|
||||
) -> AltScreenFindRun {
|
||||
let dfas = options.query.as_ref().and_then(|query| {
|
||||
RegexDFAs::new_with_config(
|
||||
query.as_str(),
|
||||
FindConfig {
|
||||
is_regex_enabled: options.is_regex_enabled,
|
||||
is_case_sensitive: options.is_case_sensitive,
|
||||
},
|
||||
)
|
||||
.ok()
|
||||
});
|
||||
|
||||
let matches = dfas
|
||||
.as_ref()
|
||||
.map(|dfas| alt_screen.find(dfas))
|
||||
.unwrap_or_default();
|
||||
let focused_match_index = (!matches.is_empty()).then_some(0);
|
||||
|
||||
AltScreenFindRun {
|
||||
dfas,
|
||||
matches,
|
||||
focused_match_index,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AltScreenFindRun {
|
||||
/// Compiled [`RegexDFAs`] for the find query.
|
||||
///
|
||||
/// If the query in `options` is Some(), this is guaranteed to be `Some()`.
|
||||
dfas: Option<RegexDFAs>,
|
||||
|
||||
/// Matches found in the alt screen.
|
||||
///
|
||||
/// Each match is a range of character indices in the alt screen grid.
|
||||
///
|
||||
/// Matches in this vector are ordered in order of decreasing recency, from "bottom" to "top".
|
||||
/// This ensures that iterating over matches occurs in the order that is expected in the UI.
|
||||
///
|
||||
/// The match at index 0 is the first match to be focused after a fresh find run - this is the
|
||||
/// match closest to the bottom of the alt screen grid.
|
||||
matches: Vec<RangeInclusive<Point>>,
|
||||
|
||||
/// The index of the currently focused match in `matches` vector.
|
||||
focused_match_index: Option<usize>,
|
||||
|
||||
/// The `FindOptions` used to configure the find run.
|
||||
options: FindOptions,
|
||||
}
|
||||
|
||||
impl AltScreenFindRun {
|
||||
pub fn options(&self) -> &FindOptions {
|
||||
&self.options
|
||||
}
|
||||
|
||||
pub fn focused_match_index(&self) -> Option<usize> {
|
||||
self.focused_match_index
|
||||
}
|
||||
|
||||
pub fn focused_match_range(&self) -> Option<&RangeInclusive<Point>> {
|
||||
self.focused_match_index
|
||||
.and_then(|index| self.matches.get(index))
|
||||
}
|
||||
|
||||
/// Returns list of all alt screen matches
|
||||
pub fn matches(&self) -> &[RangeInclusive<Point>] {
|
||||
&self.matches
|
||||
}
|
||||
|
||||
/// Focuses the next match in `matches` based on the given `direction`.
|
||||
pub(super) fn focus_next_match(&mut self, direction: FindDirection) {
|
||||
if let Some(current_focused_match_index) = self.focused_match_index() {
|
||||
let index = current_focused_match_index;
|
||||
let next_match_index = match direction {
|
||||
FindDirection::Up => {
|
||||
if index + 1 < self.matches.len() {
|
||||
index + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
FindDirection::Down => {
|
||||
if index > 0 {
|
||||
index - 1
|
||||
} else {
|
||||
self.matches.len() - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
self.focused_match_index = Some(next_match_index);
|
||||
} else if !self.matches.is_empty() {
|
||||
self.focused_match_index = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reruns the find operation with the same options on the alt screen.
|
||||
pub(super) fn rerun(mut self, alt_screen: &AltScreen) -> Self {
|
||||
let Some(dfas) = self.dfas.as_ref() else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let new_matches = alt_screen.find(dfas);
|
||||
self.matches = new_matches;
|
||||
|
||||
// If there are no more matches, reset the focused index.
|
||||
if self.matches.is_empty() {
|
||||
self.focused_match_index = None;
|
||||
} else if let Some(mut focused_match_index) = self.focused_match_index {
|
||||
// If there are matches and we had one focused before, bring it into range
|
||||
// if it isn't already.
|
||||
while focused_match_index >= self.matches.len() {
|
||||
focused_match_index = focused_match_index.saturating_sub(1);
|
||||
}
|
||||
self.focused_match_index = Some(focused_match_index);
|
||||
} else {
|
||||
// If there are matches but there wasn't an existing focused match,
|
||||
// focus the first match.
|
||||
self.focused_match_index = Some(0);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a cleared version of this run (has no matches but the same options).
|
||||
pub(super) fn cleared(mut self) -> Self {
|
||||
let new_dfas =
|
||||
self.options.query.as_ref().and_then(|query| {
|
||||
match RegexDFAs::new_with_config(
|
||||
query.as_str(),
|
||||
FindConfig {
|
||||
is_regex_enabled: self.options.is_regex_enabled,
|
||||
is_case_sensitive: self.options.is_case_sensitive,
|
||||
},
|
||||
) {
|
||||
Ok(dfas) => Some(dfas),
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to construct new RegexDFAs for cleared AltScreenFindRun: {e:?}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
self.dfas = new_dfas;
|
||||
self.matches = vec![];
|
||||
self.focused_match_index = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "alt_screen_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,81 @@
|
||||
use crate::terminal::{
|
||||
find::model::{alt_screen::run_find_on_alt_screen, FindOptions},
|
||||
model::index::Point,
|
||||
TerminalModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_alt_screen() {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.set_altscreen_active();
|
||||
mock_terminal_model.process_bytes("foo\r\nbar foo\r\nfoo");
|
||||
|
||||
let run = run_find_on_alt_screen(
|
||||
FindOptions {
|
||||
query: Some("foo".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.alt_screen(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
run.matches(),
|
||||
&[
|
||||
Point { row: 2, col: 0 }..=Point { row: 2, col: 2 },
|
||||
Point { row: 1, col: 4 }..=Point { row: 1, col: 6 },
|
||||
Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
]
|
||||
);
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_alt_screen_case_sensitive() {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.set_altscreen_active();
|
||||
mock_terminal_model.process_bytes("foo\r\nbar foo\r\nFoo");
|
||||
|
||||
let run = run_find_on_alt_screen(
|
||||
FindOptions {
|
||||
query: Some("Foo".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: true,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.alt_screen(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
run.matches(),
|
||||
&[Point { row: 2, col: 0 }..=Point { row: 2, col: 2 },]
|
||||
);
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_alt_screen_regex() {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.set_altscreen_active();
|
||||
mock_terminal_model.process_bytes("aoo\r\nbar foo\r\nboo");
|
||||
|
||||
let run = run_find_on_alt_screen(
|
||||
FindOptions {
|
||||
query: Some("[ab]oo".to_owned().into()),
|
||||
is_regex_enabled: true,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.alt_screen(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
run.matches(),
|
||||
&[
|
||||
Point { row: 2, col: 0 }..=Point { row: 2, col: 2 },
|
||||
Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
]
|
||||
);
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
}
|
||||
@@ -0,0 +1,667 @@
|
||||
//! This module implements terminal find functionality for the blocklist.
|
||||
use std::{collections::HashMap, iter, ops::RangeInclusive};
|
||||
|
||||
use itertools::Itertools;
|
||||
use warpui::{units::Lines, AppContext, EntityId};
|
||||
|
||||
use crate::terminal::{
|
||||
model::{
|
||||
block::Block,
|
||||
blocks::{
|
||||
BlockHeight, BlockHeightItem, BlockHeightSummary, BlockList, RichContentItem,
|
||||
TotalIndex,
|
||||
},
|
||||
find::{FindConfig, RegexDFAs},
|
||||
index::Point,
|
||||
terminal_model::{BlockIndex, BlockSortDirection},
|
||||
},
|
||||
GridType,
|
||||
};
|
||||
use crate::view_components::find::FindDirection;
|
||||
|
||||
use super::{
|
||||
rich_content::{FindableRichContentHandle, RichContentMatchId},
|
||||
FindOptions,
|
||||
};
|
||||
|
||||
/// Runs a find operation on the blocklist using the given `options` and returns a
|
||||
/// `BlockListFindRun` with the results.
|
||||
///
|
||||
/// If the given `options` does not contain a query, short-circuits and returns a find run with no
|
||||
/// matches.
|
||||
pub(super) fn run_find_on_block_list(
|
||||
mut options: FindOptions,
|
||||
block_list: &BlockList,
|
||||
findable_rich_content_views: &HashMap<EntityId, Box<dyn FindableRichContentHandle>>,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
ctx: &mut AppContext,
|
||||
) -> BlockListFindRun {
|
||||
let Some(dfas) = options.query.as_ref().and_then(|query| {
|
||||
RegexDFAs::new_with_config(
|
||||
query.as_str(),
|
||||
FindConfig {
|
||||
is_regex_enabled: options.is_regex_enabled,
|
||||
is_case_sensitive: options.is_case_sensitive,
|
||||
},
|
||||
)
|
||||
.ok()
|
||||
}) else {
|
||||
// Clear rich content matches.
|
||||
for rich_content_view in findable_rich_content_views.values() {
|
||||
rich_content_view.clear_matches(ctx);
|
||||
}
|
||||
return BlockListFindRun {
|
||||
options,
|
||||
block_sort_direction,
|
||||
dfas: None,
|
||||
matches: vec![],
|
||||
raw_focused_match_index: None,
|
||||
};
|
||||
};
|
||||
|
||||
let mut matches = vec![];
|
||||
|
||||
// If find in block is enabled, find matches in selected blocks only
|
||||
if let Some(blocks_to_include_in_results) = options.blocks_to_include_in_results.as_mut() {
|
||||
// Sort blocks in descending order so that the most recent block is last, which is the order we expect.
|
||||
blocks_to_include_in_results.sort_by(|i, j| j.cmp(i));
|
||||
|
||||
// Must update matches in order, from first block to last block,
|
||||
// so that matches for the latest blocks come before matches for earlier blocks.
|
||||
// Note that this is true regardless of the blocklist orientation (inverted or not)
|
||||
// In both cases we want the most recent block updated last, which means the sort direction
|
||||
// here should always be MostRecentLast
|
||||
for block_index in blocks_to_include_in_results {
|
||||
let agent_view_state = block_list.agent_view_state();
|
||||
if let Some(block) = block_list
|
||||
.block_at(*block_index)
|
||||
.filter(|block| !block.is_empty(agent_view_state))
|
||||
{
|
||||
if block.height(agent_view_state) == Lines::zero() {
|
||||
// This should not happen in practice, because `blocks_to_include_in_results`
|
||||
// is set by selecting blocks, which are presumably visible.
|
||||
continue;
|
||||
}
|
||||
matches.extend(run_find_on_block(
|
||||
&dfas,
|
||||
block,
|
||||
*block_index,
|
||||
block_sort_direction,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, loop through all the blocks in the terminal's blocklist, executing find on each block.
|
||||
run_find_on_sumtree(
|
||||
&options,
|
||||
&dfas,
|
||||
block_list,
|
||||
findable_rich_content_views,
|
||||
block_sort_direction,
|
||||
&mut matches,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
let raw_focused_match_index = (!matches.is_empty()).then_some(0);
|
||||
BlockListFindRun {
|
||||
dfas: Some(dfas),
|
||||
matches,
|
||||
raw_focused_match_index,
|
||||
options,
|
||||
block_sort_direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a find operation over blocks yielded by the given `blocks_iter`, appending `BlockListMatches`
|
||||
/// to the `matches` output parameter in the same order as blocks in the `blocks_iter`.
|
||||
fn run_find_on_sumtree(
|
||||
options: &FindOptions,
|
||||
dfas: &RegexDFAs,
|
||||
block_list: &BlockList,
|
||||
rich_content_views: &HashMap<EntityId, Box<dyn FindableRichContentHandle>>,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
matches: &mut Vec<BlockListMatch>,
|
||||
ctx: &mut AppContext,
|
||||
) {
|
||||
let mut cursor = block_list
|
||||
.block_heights()
|
||||
.cursor::<BlockHeight, BlockHeightSummary>();
|
||||
cursor.descend_to_last_item(block_list.block_heights());
|
||||
|
||||
while let Some(item) = cursor.item() {
|
||||
match item {
|
||||
BlockHeightItem::Block(height) if height.into_lines() > Lines::zero() => {
|
||||
let block_index = cursor.start().block_count;
|
||||
if let Some(block) = block_list.block_at(block_index.into()) {
|
||||
matches.extend(run_find_on_block(
|
||||
dfas,
|
||||
block,
|
||||
block_index.into(),
|
||||
block_sort_direction,
|
||||
));
|
||||
}
|
||||
}
|
||||
BlockHeightItem::RichContent(RichContentItem {
|
||||
view_id,
|
||||
last_laid_out_height,
|
||||
..
|
||||
}) if last_laid_out_height.into_lines() > Lines::zero() => {
|
||||
if let Some(findable_view) = rich_content_views.get(view_id) {
|
||||
let mut rich_content_matches = findable_view.run_find(options, ctx);
|
||||
if matches!(block_sort_direction, BlockSortDirection::MostRecentLast) {
|
||||
rich_content_matches.reverse();
|
||||
}
|
||||
|
||||
matches.extend(rich_content_matches.into_iter().map(|match_id| {
|
||||
BlockListMatch::RichContent {
|
||||
match_id,
|
||||
view_id: *view_id,
|
||||
index: cursor.start().total_count.into(),
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
cursor.prev();
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs a find operation on the given block and returns the resulting vector of `BlockListMatch`es.
|
||||
fn run_find_on_block(
|
||||
dfas: &RegexDFAs,
|
||||
block: &Block,
|
||||
block_index: BlockIndex,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
) -> Vec<BlockListMatch> {
|
||||
let grid_order = match block_sort_direction {
|
||||
BlockSortDirection::MostRecentFirst => &[GridType::PromptAndCommand, GridType::Output],
|
||||
BlockSortDirection::MostRecentLast => &[GridType::Output, GridType::PromptAndCommand],
|
||||
};
|
||||
|
||||
let mut block_matches = vec![];
|
||||
for grid_type in grid_order.iter() {
|
||||
let mut grid_matches = match grid_type {
|
||||
GridType::PromptAndCommand => block
|
||||
.find_prompt_and_command_grid_matches(dfas)
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range,
|
||||
block_index,
|
||||
is_filtered: false,
|
||||
})
|
||||
})
|
||||
.collect_vec(),
|
||||
GridType::Output => {
|
||||
let mut output_grid_matches = block
|
||||
.find_output_grid_matches(dfas)
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range,
|
||||
block_index,
|
||||
is_filtered: false,
|
||||
})
|
||||
})
|
||||
.collect_vec();
|
||||
update_matches_for_filtered_block(
|
||||
output_grid_matches.iter_mut(),
|
||||
block,
|
||||
block_sort_direction,
|
||||
);
|
||||
output_grid_matches
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
if matches!(block_sort_direction, BlockSortDirection::MostRecentFirst) {
|
||||
grid_matches.reverse();
|
||||
}
|
||||
block_matches.extend(grid_matches);
|
||||
}
|
||||
block_matches
|
||||
}
|
||||
|
||||
/// Represents a single find match in a grid-based block in the blocklist..
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlockGridMatch {
|
||||
/// The type of grid in which the match was found.
|
||||
pub grid_type: GridType,
|
||||
|
||||
/// The character index range of the match.
|
||||
pub range: RangeInclusive<Point>,
|
||||
|
||||
/// The index of the containing block.
|
||||
pub block_index: BlockIndex,
|
||||
|
||||
/// `true` if the match should be filtered out from displayed results (e.g. if the containing
|
||||
/// row has been filtered out via block filtering).
|
||||
pub is_filtered: bool,
|
||||
}
|
||||
|
||||
/// Represents a single find match in the blocklist.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BlockListMatch {
|
||||
CommandBlock(BlockGridMatch),
|
||||
RichContent {
|
||||
match_id: RichContentMatchId,
|
||||
view_id: EntityId,
|
||||
index: TotalIndex,
|
||||
},
|
||||
}
|
||||
|
||||
impl BlockListMatch {
|
||||
pub fn is_filtered(&self) -> bool {
|
||||
match self {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch { is_filtered, .. }) => *is_filtered,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_type(&self) -> Option<GridType> {
|
||||
match self {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch { grid_type, .. }) => Some(*grid_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches_block(&self, block_index: BlockIndex) -> bool {
|
||||
match self {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
block_index: match_block_index,
|
||||
..
|
||||
}) => block_index == *match_block_index,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_blockgrid(&self, block_index: BlockIndex, grid_type: GridType) -> bool {
|
||||
match self {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
block_index: match_block_index,
|
||||
grid_type: match_grid_type,
|
||||
..
|
||||
}) => block_index == *match_block_index && grid_type == *match_grid_type,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of a find "run" on the blocklist.
|
||||
#[derive(Debug)]
|
||||
pub struct BlockListFindRun {
|
||||
/// Compiled [`RegexDFAs`] for the find query.
|
||||
///
|
||||
/// If the query in `options` is Some(), this is guaranteed to be `Some()`.
|
||||
dfas: Option<RegexDFAs>,
|
||||
|
||||
/// Matches found in the blocklist.
|
||||
///
|
||||
/// Matches in this vector are ordered by block index in order of decreasing recency. Within
|
||||
/// the slice for a given block, matches ordering depends on the `block_sort_direction`. For
|
||||
/// `BlockSortDirection::MostRecentLast`, matches are ordered from "bottom" to "top". For
|
||||
/// `BlockSortDirection::MostRecentFirst`, matches are ordered from "top" to "bottom". This
|
||||
/// ensures that iterating over matches occurs in the order that is expected in the UI.
|
||||
///
|
||||
/// The match at index 0 is the first match to be focused after a fresh find run - for
|
||||
/// pin-to-bottom and waterfall input modes, this is the match closest to the bottom of the
|
||||
/// most recent block. For pin-to-top, this is the match closest to the top of the most recent
|
||||
/// block.
|
||||
matches: Vec<BlockListMatch>,
|
||||
|
||||
/// The index of the currently focused match in the `matches` vector.
|
||||
///
|
||||
/// Note that this may differ from the focused match index displayed in the find bar UI, since
|
||||
/// the number of visible matches may be affected by block filtering. The focused match index
|
||||
/// in the UI thus is relative to visible matches, while this field is relative to all matches.
|
||||
raw_focused_match_index: Option<usize>,
|
||||
|
||||
/// The `FindOptions` used to configure the find run.
|
||||
options: FindOptions,
|
||||
|
||||
/// The block sort direction, reflected in the ordering of intra-block matches in `matches`.
|
||||
block_sort_direction: BlockSortDirection,
|
||||
}
|
||||
|
||||
impl BlockListFindRun {
|
||||
/// Returns the UI focused match index, relative to the list of visible matches.
|
||||
pub fn focused_match_index(&self) -> Option<usize> {
|
||||
self.raw_focused_match_index.map(|focused_index| {
|
||||
focused_index
|
||||
- self.matches[..focused_index]
|
||||
.iter()
|
||||
.fold(0, |count, find_match| {
|
||||
if find_match.is_filtered() {
|
||||
count + 1
|
||||
} else {
|
||||
count
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator over visible `BlockListMatch`es.
|
||||
pub fn matches(&self) -> impl Iterator<Item = &BlockListMatch> {
|
||||
self.matches.iter().filter(|m| !m.is_filtered())
|
||||
}
|
||||
|
||||
/// Returns an iterator over matches for the given block index and grid type, in "ascending"
|
||||
/// order.
|
||||
///
|
||||
/// This logic relies on the ordering of `self.matches` explained in the field declaration.
|
||||
pub fn matches_for_block_grid(
|
||||
&self,
|
||||
block_index: BlockIndex,
|
||||
grid_type: GridType,
|
||||
) -> Box<dyn Iterator<Item = &RangeInclusive<Point>> + '_> {
|
||||
let Some(start_index) = self
|
||||
.matches
|
||||
.iter()
|
||||
.position(|m| m.matches_blockgrid(block_index, grid_type))
|
||||
else {
|
||||
return Box::new(iter::empty());
|
||||
};
|
||||
let match_slice = if let Some(relative_end_index) = self.matches[start_index..]
|
||||
.iter()
|
||||
.position(|m| !m.matches_blockgrid(block_index, grid_type))
|
||||
{
|
||||
&self.matches[start_index..(start_index + relative_end_index)]
|
||||
} else {
|
||||
&self.matches[start_index..]
|
||||
};
|
||||
match self.block_sort_direction {
|
||||
BlockSortDirection::MostRecentLast => Box::new(
|
||||
match_slice
|
||||
.iter()
|
||||
.filter_map(|m| match m {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range, is_filtered, ..
|
||||
}) => {
|
||||
if !is_filtered {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.rev(),
|
||||
),
|
||||
BlockSortDirection::MostRecentFirst => {
|
||||
Box::new(match_slice.iter().filter_map(|m| match m {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range, is_filtered, ..
|
||||
}) => {
|
||||
if !is_filtered {
|
||||
Some(range)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_match(&self) -> Option<&BlockListMatch> {
|
||||
self.raw_focused_match_index
|
||||
.and_then(|i| self.matches.get(i))
|
||||
}
|
||||
|
||||
pub fn options(&self) -> &FindOptions {
|
||||
&self.options
|
||||
}
|
||||
|
||||
/// Focuses the next match in `matches` based on the given `direction` and `block_sort_direction`.
|
||||
pub(super) fn focus_next_match(
|
||||
&mut self,
|
||||
direction: FindDirection,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
) {
|
||||
let new_focused_index = match (self.raw_focused_match_index, self.matches.is_empty()) {
|
||||
(_, true) => None,
|
||||
(Some(mut current_index), false) => {
|
||||
let mut new_focused_index = None;
|
||||
for _ in 0..self.matches.len() {
|
||||
current_index = match (direction, block_sort_direction) {
|
||||
(FindDirection::Up, BlockSortDirection::MostRecentLast)
|
||||
| (FindDirection::Down, BlockSortDirection::MostRecentFirst) => {
|
||||
if current_index + 1 < self.matches.len() {
|
||||
current_index + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
(FindDirection::Down, BlockSortDirection::MostRecentLast)
|
||||
| (FindDirection::Up, BlockSortDirection::MostRecentFirst) => {
|
||||
if current_index > 0 {
|
||||
current_index - 1
|
||||
} else {
|
||||
self.matches.len() - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
if !self.matches[current_index].is_filtered() {
|
||||
new_focused_index = Some(current_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_focused_index
|
||||
}
|
||||
(None, false) => Some(0),
|
||||
};
|
||||
self.raw_focused_match_index = new_focused_index;
|
||||
}
|
||||
|
||||
/// Reruns the find operation on the block at the given index, updates the matches with the new results.
|
||||
///
|
||||
/// Note that matches for a given block are expected to appear in a contiguous slice (per the
|
||||
/// expected ordering of `self.matches`).
|
||||
pub(super) fn rerun_on_block(
|
||||
mut self,
|
||||
block: &Block,
|
||||
block_index: BlockIndex,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
) -> Self {
|
||||
let Some(dfas) = self.dfas.as_ref() else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let old_block_matches_start_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.position(|find_match| find_match.matches_block(block_index));
|
||||
let mut new_matches = run_find_on_block(dfas, block, block_index, block_sort_direction);
|
||||
if let Some(start_index) = old_block_matches_start_index {
|
||||
let end_index = old_block_matches_start_index
|
||||
.and_then(|i| {
|
||||
self.matches[(i + 1)..]
|
||||
.iter()
|
||||
.position(|find_match| !find_match.matches_block(block_index))
|
||||
.map(|j| i + j + 1)
|
||||
})
|
||||
.unwrap_or(self.matches.len());
|
||||
|
||||
// Splice in the new matches where the old block matches used to exist.
|
||||
self.matches.splice(start_index..end_index, new_matches);
|
||||
} else {
|
||||
new_matches.append(&mut self.matches);
|
||||
self.matches = new_matches;
|
||||
}
|
||||
|
||||
if self.matches.is_empty() {
|
||||
self.raw_focused_match_index = None;
|
||||
} else if let Some(mut focused_match_index) = self.raw_focused_match_index {
|
||||
// Ensure the focused match index is still valid.
|
||||
while focused_match_index >= self.matches.len() {
|
||||
focused_match_index = focused_match_index.saturating_sub(1);
|
||||
}
|
||||
self.raw_focused_match_index = Some(focused_match_index);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn update_matches_for_filtered_block(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
block_index: BlockIndex,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
) {
|
||||
update_matches_for_filtered_block(
|
||||
self.matches.iter_mut().filter(|find_match| {
|
||||
find_match.matches_block(block_index)
|
||||
&& matches!(find_match.grid_type(), Some(GridType::Output))
|
||||
}),
|
||||
block,
|
||||
block_sort_direction,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn cleared(mut self) -> Self {
|
||||
let new_dfas =
|
||||
self.options.query.as_ref().and_then(|query| {
|
||||
match RegexDFAs::new_with_config(
|
||||
query.as_str(),
|
||||
FindConfig {
|
||||
is_regex_enabled: self.options.is_regex_enabled,
|
||||
is_case_sensitive: self.options.is_case_sensitive,
|
||||
},
|
||||
) {
|
||||
Ok(dfas) => Some(dfas),
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to construct new RegexDFAs for cleared BlockListFindRun: {e:?}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
self.dfas = new_dfas;
|
||||
self.matches = vec![];
|
||||
self.raw_focused_match_index = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn update_matches_for_filtered_block<'a>(
|
||||
mut matches: impl Iterator<Item = &'a mut BlockListMatch>,
|
||||
block: &Block,
|
||||
block_sort_direction: BlockSortDirection,
|
||||
) {
|
||||
let Some(displayed_rows) = block.displayed_output_row_ranges() else {
|
||||
matches.for_each(|find_match| {
|
||||
if let BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
ref mut is_filtered,
|
||||
..
|
||||
}) = find_match
|
||||
{
|
||||
*is_filtered = false;
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
match block_sort_direction {
|
||||
BlockSortDirection::MostRecentLast => {
|
||||
let mut current_find_match = matches.next();
|
||||
|
||||
let mut displayed_row_ranges = displayed_rows.rev();
|
||||
let mut current_row_range = displayed_row_ranges.next();
|
||||
|
||||
loop {
|
||||
match (current_find_match.take(), current_row_range.take()) {
|
||||
(Some(find_match), Some(row_range)) => {
|
||||
match find_match {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range,
|
||||
is_filtered,
|
||||
..
|
||||
}) if range.end().row > *row_range.end() => {
|
||||
*is_filtered = true;
|
||||
current_find_match = matches.next();
|
||||
current_row_range = Some(row_range);
|
||||
}
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range,
|
||||
is_filtered,
|
||||
..
|
||||
}) if range.start().row >= *row_range.start() => {
|
||||
*is_filtered = false;
|
||||
current_find_match = matches.next();
|
||||
current_row_range = Some(row_range);
|
||||
}
|
||||
_ => {
|
||||
current_find_match = Some(find_match);
|
||||
current_row_range = displayed_row_ranges.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
(Some(find_match), None) => {
|
||||
if let BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
is_filtered, ..
|
||||
}) = find_match
|
||||
{
|
||||
*is_filtered = true;
|
||||
}
|
||||
current_find_match = matches.next();
|
||||
}
|
||||
(None, _) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
BlockSortDirection::MostRecentFirst => {
|
||||
let mut current_find_match = matches.next();
|
||||
|
||||
let mut displayed_row_ranges = displayed_rows;
|
||||
let mut current_row_range = displayed_row_ranges.next();
|
||||
|
||||
loop {
|
||||
match (current_find_match.take(), current_row_range.take()) {
|
||||
(Some(find_match), Some(row_range)) => {
|
||||
match find_match {
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range,
|
||||
is_filtered,
|
||||
..
|
||||
}) if range.start().row < *row_range.start() => {
|
||||
*is_filtered = true;
|
||||
current_find_match = matches.next();
|
||||
}
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
range,
|
||||
is_filtered,
|
||||
..
|
||||
}) if range.end().row <= *row_range.end() => {
|
||||
*is_filtered = false;
|
||||
current_find_match = matches.next();
|
||||
}
|
||||
_ => {
|
||||
current_find_match = Some(find_match);
|
||||
current_row_range = displayed_row_ranges.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
(Some(find_match), None) => {
|
||||
if let BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
is_filtered, ..
|
||||
}) = find_match
|
||||
{
|
||||
*is_filtered = true;
|
||||
}
|
||||
current_find_match = matches.next();
|
||||
}
|
||||
(None, _) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "block_list_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,355 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
use warpui::App;
|
||||
|
||||
use crate::terminal::{
|
||||
block_filter::BlockFilterQuery,
|
||||
find::{
|
||||
model::{block_list::run_find_on_block_list, FindOptions},
|
||||
BlockGridMatch,
|
||||
},
|
||||
model::{index::Point, terminal_model::BlockSortDirection},
|
||||
GridType, TerminalModel,
|
||||
};
|
||||
|
||||
use super::{BlockListFindRun, BlockListMatch};
|
||||
|
||||
impl BlockListFindRun {
|
||||
fn all_matches(&self) -> &[BlockListMatch] {
|
||||
&self.matches[..]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_block_list() {
|
||||
App::test((), |mut app| async move {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
|
||||
mock_terminal_model.simulate_block("barbaz", "bar baz\r\n");
|
||||
|
||||
let run = app.update(|ctx| {
|
||||
run_find_on_block_list(
|
||||
FindOptions {
|
||||
query: Some("bar".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.block_list(),
|
||||
&HashMap::new(),
|
||||
BlockSortDirection::MostRecentLast,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
// Matches should be sorted by recency at the block level, then "bottom to top" within each
|
||||
// block.
|
||||
assert_eq!(
|
||||
run.matches().collect_vec(),
|
||||
vec![
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_block_list_pin_to_top() {
|
||||
App::test((), |mut app| async move {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
|
||||
mock_terminal_model.simulate_block("barbaz", "bar baz\r\n");
|
||||
|
||||
let run = app.update(|ctx| {
|
||||
run_find_on_block_list(
|
||||
FindOptions {
|
||||
query: Some("bar".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.block_list(),
|
||||
&HashMap::new(),
|
||||
BlockSortDirection::MostRecentFirst,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
// Matches should be sorted by recency at the block level, then "top to bottom" within each
|
||||
// block.
|
||||
assert_eq!(
|
||||
run.matches().collect_vec(),
|
||||
vec![
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_block_list_case_sensitive() {
|
||||
App::test((), |mut app| async move {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
|
||||
mock_terminal_model.simulate_block("Barbaz", "Bar baz\r\n");
|
||||
|
||||
let run = app.update(|ctx| {
|
||||
run_find_on_block_list(
|
||||
FindOptions {
|
||||
query: Some("Bar".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: true,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.block_list(),
|
||||
&HashMap::new(),
|
||||
BlockSortDirection::MostRecentLast,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
// Matches should be sorted by recency at the block level, then "bottom to top" within each
|
||||
// block.
|
||||
assert_eq!(
|
||||
run.matches().collect_vec(),
|
||||
vec![
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_block_list_regex_enabled() {
|
||||
App::test((), |mut app| async move {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
|
||||
mock_terminal_model.simulate_block("barbaz", "bar baz\r\n");
|
||||
|
||||
let run = app.update(|ctx| {
|
||||
run_find_on_block_list(
|
||||
FindOptions {
|
||||
query: Some("ba[rz]".to_owned().into()),
|
||||
is_regex_enabled: true,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.block_list(),
|
||||
&HashMap::new(),
|
||||
BlockSortDirection::MostRecentLast,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
// Matches should be sorted by recency at the block level, then "bottom to top" within each
|
||||
// block.
|
||||
assert_eq!(
|
||||
run.matches().collect_vec(),
|
||||
vec![
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 4 }..=Point { row: 0, col: 6 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
assert_eq!(run.focused_match_index(), Some(0));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_find_on_block_list_with_filtered_block() {
|
||||
App::test((), |mut app| async move {
|
||||
let mut mock_terminal_model = TerminalModel::mock(None, None);
|
||||
mock_terminal_model.simulate_block("foobar", "foo\r\nbar\r\n");
|
||||
mock_terminal_model.simulate_block("barbaz filter", "bar baz\r\nbar bat\r\nbar baz\r\n");
|
||||
|
||||
// Filter the second block to only show lines containing "bat".
|
||||
mock_terminal_model
|
||||
.update_filter_on_block(2.into(), BlockFilterQuery::new_for_test("bat".to_owned()));
|
||||
|
||||
let run = app.update(|ctx| {
|
||||
run_find_on_block_list(
|
||||
FindOptions {
|
||||
query: Some("bar".to_owned().into()),
|
||||
is_regex_enabled: false,
|
||||
is_case_sensitive: false,
|
||||
..Default::default()
|
||||
},
|
||||
mock_terminal_model.block_list(),
|
||||
&HashMap::new(),
|
||||
BlockSortDirection::MostRecentLast,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
// Matches should be sorted by recency at the block level, then "bottom to top" within each
|
||||
// block.
|
||||
//
|
||||
// The first and third rows of the most recent block contain baz, and should be filtered.
|
||||
assert_eq!(
|
||||
run.matches().collect_vec(),
|
||||
vec![
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
&BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
// Check `all_matches` so we can ensure the filtered matches exist, but were filtered.
|
||||
assert_eq!(
|
||||
run.all_matches(),
|
||||
&[
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 2, col: 0 }..=Point { row: 2, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: true,
|
||||
}),
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: true,
|
||||
}),
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 0 }..=Point { row: 0, col: 2 },
|
||||
block_index: 2.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::Output,
|
||||
range: Point { row: 1, col: 0 }..=Point { row: 1, col: 2 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
grid_type: GridType::PromptAndCommand,
|
||||
range: Point { row: 0, col: 3 }..=Point { row: 0, col: 5 },
|
||||
block_index: 1.into(),
|
||||
is_filtered: false,
|
||||
}),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Traits and abstractions supporting the find operation across rich content blocks.
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use warpui::{AppContext, View, ViewContext, ViewHandle};
|
||||
|
||||
use super::FindOptions;
|
||||
|
||||
/// Unique ID for a find match in a rich content view.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct RichContentMatchId(usize);
|
||||
|
||||
impl Default for RichContentMatchId {
|
||||
fn default() -> Self {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
Self(COUNTER.fetch_add(1, Ordering::SeqCst))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to be implemented by blocklist rich content view to support find operations.
|
||||
pub trait FindableRichContentView: View {
|
||||
/// Runs a find operation configured with the given `options` on the view and returns
|
||||
/// a list of match IDs corresponding to found matches.
|
||||
///
|
||||
/// The view is responsible for actually storing matches (in whatever representation makes
|
||||
/// sense for the given view) and maintaining a mapping between the returned match IDs and the
|
||||
/// internal matches.
|
||||
///
|
||||
/// As such, the view is also responsible for rendering the appropriate UI for highlighted
|
||||
/// matches.
|
||||
fn run_find(
|
||||
&mut self,
|
||||
options: &FindOptions,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Vec<RichContentMatchId>;
|
||||
|
||||
/// Clears cached matches stored in a previous `run_find` call, if necessary.
|
||||
fn clear_matches(&mut self, ctx: &mut ViewContext<Self>);
|
||||
}
|
||||
|
||||
/// Wrapper trait around `RichContentView` that enables storing a homogenous collection of
|
||||
/// `RichContentView` implementations (via their corresponding `ViewHandle`s).
|
||||
///
|
||||
/// Simply directs each method call to the corresponding `FindableRichContentView` call.
|
||||
///
|
||||
/// New rich content views do _not_ require a new `FindableRichContentHandle` implementation;
|
||||
/// this is an implementation detail of the `FindModel`-internal usage of the
|
||||
/// `FindableRichContentView` trait.
|
||||
pub(super) trait FindableRichContentHandle {
|
||||
fn run_find(&self, options: &FindOptions, ctx: &mut AppContext) -> Vec<RichContentMatchId>;
|
||||
|
||||
fn clear_matches(&self, ctx: &mut AppContext);
|
||||
}
|
||||
|
||||
/// Blanket implementation of `FindableRichContentHandle` for any handles of view type that
|
||||
/// implements `FindableRichContentView`.
|
||||
impl<F> FindableRichContentHandle for ViewHandle<F>
|
||||
where
|
||||
F: FindableRichContentView,
|
||||
{
|
||||
fn run_find(&self, options: &FindOptions, ctx: &mut AppContext) -> Vec<RichContentMatchId> {
|
||||
self.update(ctx, |me, ctx| me.run_find(options, ctx))
|
||||
}
|
||||
|
||||
fn clear_matches(&self, ctx: &mut AppContext) {
|
||||
self.update(ctx, |me, ctx| me.clear_matches(ctx));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Exports helper test-only methods for use in unit and integration tests.
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::terminal::model::terminal_model::BlockIndex;
|
||||
|
||||
use super::{block_list::BlockListMatch, BlockListFindRun, TerminalFindModel};
|
||||
|
||||
impl TerminalFindModel {
|
||||
pub fn visible_block_list_match_count(&self) -> usize {
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
.map(|run| {
|
||||
run.matches()
|
||||
.filter(|find_match| !find_match.is_filtered())
|
||||
.collect_vec()
|
||||
.len()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockListFindRun {
|
||||
pub fn matches_for_block(&self, index: BlockIndex) -> impl Iterator<Item = &BlockListMatch> {
|
||||
self.matches()
|
||||
.filter(move |find_match| find_match.matches_block(index))
|
||||
}
|
||||
|
||||
pub fn focused_match_block_index(&self) -> Option<BlockIndex> {
|
||||
self.focused_match_index().and_then(|index| {
|
||||
self.matches().nth(index).and_then(|m| {
|
||||
if let BlockListMatch::CommandBlock(block_match) = m {
|
||||
Some(block_match.block_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user