first pass of merging in warp (doesn't build)
This commit is contained in:
+508
-62
@@ -1,31 +1,176 @@
|
||||
mod alt_screen;
|
||||
pub mod async_find;
|
||||
mod block_list;
|
||||
#[allow(dead_code)]
|
||||
mod rich_content;
|
||||
#[cfg(any(test, feature = "integration_tests"))]
|
||||
mod testing;
|
||||
|
||||
pub use block_list::{BlockGridMatch, BlockListFindRun, BlockListMatch};
|
||||
pub use rich_content::{FindableRichContentView, RichContentMatchId};
|
||||
|
||||
use crate::terminal::block_list_viewport::InputMode;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::Arc;
|
||||
|
||||
use alt_screen::{run_find_on_alt_screen, AltScreenFindRun};
|
||||
use galaxyui::{AppContext, Entity, EntityId, ModelContext, SingletonEntity, ViewHandle};
|
||||
pub use async_find::{AsyncFindController, AsyncFindStatus};
|
||||
use block_list::run_find_on_block_list;
|
||||
pub use block_list::{BlockGridMatch, BlockListFindRun, BlockListMatch};
|
||||
use parking_lot::FairMutex;
|
||||
use rich_content::FindableRichContentHandle;
|
||||
pub use rich_content::{FindableRichContentView, RichContentMatchId};
|
||||
use settings::Setting as _;
|
||||
|
||||
use crate::{
|
||||
settings::InputModeSettings,
|
||||
terminal::model::{terminal_model::BlockIndex, TerminalModel},
|
||||
view_components::find::{FindEvent, FindModel},
|
||||
};
|
||||
use crate::settings::InputModeSettings;
|
||||
use crate::terminal::block_list_element::GridType;
|
||||
use crate::terminal::block_list_viewport::InputMode;
|
||||
use crate::terminal::model::grid::grid_handler::GridHandler;
|
||||
use crate::terminal::model::index::Point;
|
||||
use crate::terminal::model::terminal_model::BlockIndex;
|
||||
use crate::terminal::model::TerminalModel;
|
||||
use crate::terminal::settings::TerminalSettings;
|
||||
use crate::view_components::find::{FindDirection, FindEvent, FindModel};
|
||||
|
||||
use crate::view_components::find::FindDirection;
|
||||
/// Pre-computed find data for rendering a single block.
|
||||
///
|
||||
/// This struct provides a unified interface for both sync (`BlockListFindRun`) and async
|
||||
/// (`AsyncFindController`) find paths, allowing the rendering code to work with either.
|
||||
///
|
||||
/// Stores references to the underlying data source and provides methods to create iterators
|
||||
/// on demand (since iterators can only be consumed once).
|
||||
pub enum BlockFindRenderData<'a> {
|
||||
/// Data from the synchronous find path.
|
||||
Sync {
|
||||
run: &'a BlockListFindRun,
|
||||
block_index: BlockIndex,
|
||||
},
|
||||
/// Data from the asynchronous find path.
|
||||
///
|
||||
/// For the async path, we pre-compute and store converted matches since they use
|
||||
/// absolute coordinates internally and need conversion to relative Points.
|
||||
Async {
|
||||
/// Pre-converted command grid matches (filtered for truncation).
|
||||
command_matches: Vec<RangeInclusive<Point>>,
|
||||
/// Pre-converted output grid matches (filtered for truncation).
|
||||
output_matches: Vec<RangeInclusive<Point>>,
|
||||
/// Focused range in command grid, if any.
|
||||
focused_command_range: Option<RangeInclusive<Point>>,
|
||||
/// Focused range in output grid, if any.
|
||||
focused_output_range: Option<RangeInclusive<Point>>,
|
||||
},
|
||||
}
|
||||
|
||||
use block_list::run_find_on_block_list;
|
||||
use rich_content::FindableRichContentHandle;
|
||||
impl<'a> BlockFindRenderData<'a> {
|
||||
/// Creates render data from the sync `BlockListFindRun`.
|
||||
pub fn from_sync(run: &'a BlockListFindRun, block_index: BlockIndex) -> Self {
|
||||
Self::Sync { run, block_index }
|
||||
}
|
||||
|
||||
/// Creates render data from the async `AsyncFindController`.
|
||||
///
|
||||
/// This pre-converts matches from absolute to relative coordinates, filtering out
|
||||
/// any matches that have been truncated from scrollback.
|
||||
pub fn from_async(
|
||||
controller: &'a AsyncFindController,
|
||||
block_index: BlockIndex,
|
||||
command_grid: Option<&GridHandler>,
|
||||
output_grid: Option<&GridHandler>,
|
||||
) -> Self {
|
||||
// Convert command grid matches.
|
||||
let command_matches = command_grid
|
||||
.and_then(|grid| {
|
||||
controller
|
||||
.matches_for_block_grid(block_index, GridType::PromptAndCommand)
|
||||
.map(|matches| {
|
||||
matches
|
||||
.iter()
|
||||
.filter_map(|m| m.to_range(grid))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Convert output grid matches.
|
||||
let output_matches = output_grid
|
||||
.and_then(|grid| {
|
||||
controller
|
||||
.matches_for_block_grid(block_index, GridType::Output)
|
||||
.map(|matches| {
|
||||
matches
|
||||
.iter()
|
||||
.filter_map(|m| m.to_range(grid))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Get focused match ranges.
|
||||
let focused_match = controller.focused_terminal_match();
|
||||
let focused_command_range = focused_match
|
||||
.as_ref()
|
||||
.filter(|m| m.block_index == block_index && m.grid_type == GridType::PromptAndCommand)
|
||||
.and_then(|m| command_grid.and_then(|grid| m.range.to_range(grid)));
|
||||
let focused_output_range = focused_match
|
||||
.as_ref()
|
||||
.filter(|m| m.block_index == block_index && m.grid_type == GridType::Output)
|
||||
.and_then(|m| output_grid.and_then(|grid| m.range.to_range(grid)));
|
||||
|
||||
Self::Async {
|
||||
command_matches,
|
||||
output_matches,
|
||||
focused_command_range,
|
||||
focused_output_range,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over match ranges for the command grid.
|
||||
pub fn command_grid_matches(
|
||||
&self,
|
||||
) -> Option<Box<dyn Iterator<Item = &RangeInclusive<Point>> + '_>> {
|
||||
match self {
|
||||
Self::Sync { run, block_index } => {
|
||||
Some(run.matches_for_block_grid(*block_index, GridType::PromptAndCommand))
|
||||
}
|
||||
Self::Async {
|
||||
command_matches, ..
|
||||
} => Some(Box::new(command_matches.iter())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over match ranges for the output grid.
|
||||
pub fn output_grid_matches(
|
||||
&self,
|
||||
) -> Option<Box<dyn Iterator<Item = &RangeInclusive<Point>> + '_>> {
|
||||
match self {
|
||||
Self::Sync { run, block_index } => {
|
||||
Some(run.matches_for_block_grid(*block_index, GridType::Output))
|
||||
}
|
||||
Self::Async { output_matches, .. } => Some(Box::new(output_matches.iter())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the focused match range if it's in the specified grid.
|
||||
pub fn focused_range_for_grid(&self, grid_type: GridType) -> Option<RangeInclusive<Point>> {
|
||||
match self {
|
||||
Self::Sync { run, block_index } => run.focused_match().and_then(|m| match m {
|
||||
BlockListMatch::CommandBlock(grid_match)
|
||||
if grid_match.block_index == *block_index
|
||||
&& grid_match.grid_type == grid_type =>
|
||||
{
|
||||
Some(grid_match.range.clone())
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
Self::Async {
|
||||
focused_command_range,
|
||||
focused_output_range,
|
||||
..
|
||||
} => match grid_type {
|
||||
GridType::PromptAndCommand => focused_command_range.clone(),
|
||||
GridType::Output => focused_output_range.clone(),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `TerminalView`-scoped model for the find bar.
|
||||
pub struct TerminalFindModel {
|
||||
@@ -36,11 +181,14 @@ pub struct TerminalFindModel {
|
||||
/// The most recent find "run" on the alt screen, if any.
|
||||
alt_screen_find_run: Option<AltScreenFindRun>,
|
||||
|
||||
/// The most recent find "run" on the block list, if any.
|
||||
/// The most recent find "run" on the block list, if any (sync path).
|
||||
block_list_find_run: Option<BlockListFindRun>,
|
||||
|
||||
/// `true` if the find bar is open.
|
||||
is_find_bar_open: bool,
|
||||
|
||||
/// Controller for async find operations.
|
||||
pub(crate) async_find_controller: Option<AsyncFindController>,
|
||||
}
|
||||
|
||||
impl FindModel for TerminalFindModel {
|
||||
@@ -49,6 +197,8 @@ impl FindModel for TerminalFindModel {
|
||||
self.alt_screen_find_run
|
||||
.as_ref()
|
||||
.and_then(|run| run.focused_match_index())
|
||||
} else if let Some(controller) = &self.async_find_controller {
|
||||
controller.focused_match_index()
|
||||
} else {
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
@@ -62,6 +212,8 @@ impl FindModel for TerminalFindModel {
|
||||
.as_ref()
|
||||
.map(|run| run.matches().len())
|
||||
.unwrap_or(0)
|
||||
} else if let Some(controller) = &self.async_find_controller {
|
||||
controller.match_count()
|
||||
} else {
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
@@ -77,24 +229,42 @@ impl FindModel for TerminalFindModel {
|
||||
InputMode::PinnedToTop => FindDirection::Down,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_scanning(&self) -> bool {
|
||||
self.is_async_find_scanning()
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalFindModel {
|
||||
pub fn new(terminal_model: Arc<FairMutex<TerminalModel>>) -> Self {
|
||||
pub fn new(terminal_model: Arc<FairMutex<TerminalModel>>, ctx: &AppContext) -> Self {
|
||||
let async_find_controller = if TerminalSettings::as_ref(ctx).is_async_find_enabled() {
|
||||
Some(AsyncFindController::new(terminal_model.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
terminal_model,
|
||||
rich_content_views: HashMap::new(),
|
||||
alt_screen_find_run: None,
|
||||
block_list_find_run: None,
|
||||
is_find_bar_open: false,
|
||||
async_find_controller,
|
||||
}
|
||||
}
|
||||
pub fn register_findable_rich_content_view<T: FindableRichContentView>(
|
||||
&mut self,
|
||||
view_handle: ViewHandle<T>,
|
||||
) {
|
||||
self.rich_content_views
|
||||
.insert(view_handle.id(), Box::new(view_handle));
|
||||
let view_id = view_handle.id();
|
||||
let boxed_handle: Box<dyn FindableRichContentHandle> = Box::new(view_handle.clone());
|
||||
|
||||
// Register with async find controller if enabled.
|
||||
if let Some(controller) = &mut self.async_find_controller {
|
||||
controller.register_rich_content_view(view_id, Box::new(view_handle));
|
||||
}
|
||||
|
||||
self.rich_content_views.insert(view_id, boxed_handle);
|
||||
}
|
||||
|
||||
/// Returns `true` if the find bar is currently open.
|
||||
@@ -117,10 +287,115 @@ impl TerminalFindModel {
|
||||
self.block_list_find_run.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the currently focused match as a `BlockListMatch`.
|
||||
///
|
||||
/// This works for both sync and async find paths.
|
||||
pub(crate) fn focused_block_list_match(&self) -> Option<BlockListMatch> {
|
||||
let model = self.terminal_model.lock();
|
||||
if model.is_alt_screen_active() {
|
||||
// Alt screen doesn't use BlockListMatch.
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(controller) = &self.async_find_controller {
|
||||
// Async path: the focused match is either a terminal match or an
|
||||
// AI match (or neither). Try each in turn and synthesize the
|
||||
// corresponding `BlockListMatch` variant so consumers don't need
|
||||
// to know which path produced the focus.
|
||||
if let Some(async_match) = controller.focused_terminal_match() {
|
||||
let block = model.block_list().block_at(async_match.block_index)?;
|
||||
let grid = match async_match.grid_type {
|
||||
GridType::PromptAndCommand => block.prompt_and_command_grid().grid_handler(),
|
||||
GridType::Output => block.output_grid().grid_handler(),
|
||||
_ => return None,
|
||||
};
|
||||
let range = async_match.range.to_range(grid)?;
|
||||
return Some(BlockListMatch::CommandBlock(BlockGridMatch {
|
||||
block_index: async_match.block_index,
|
||||
grid_type: async_match.grid_type,
|
||||
range,
|
||||
is_filtered: false,
|
||||
}));
|
||||
}
|
||||
if let Some(ai_match) = controller.focused_ai_match() {
|
||||
return Some(BlockListMatch::RichContent {
|
||||
match_id: ai_match.match_id,
|
||||
view_id: ai_match.view_id,
|
||||
index: ai_match.total_index,
|
||||
});
|
||||
}
|
||||
None
|
||||
} else {
|
||||
// Sync path: get from block_list_find_run.
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
.and_then(|run| run.focused_match())
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the focused rich content (AI) match id, if any.
|
||||
///
|
||||
/// This works for both sync and async find paths and is used by AI block
|
||||
/// rendering to apply the focused-match highlight color.
|
||||
pub(crate) fn focused_rich_content_match_id(&self) -> Option<RichContentMatchId> {
|
||||
if self.terminal_model.lock().is_alt_screen_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(controller) = &self.async_find_controller {
|
||||
controller.focused_ai_match().map(|m| m.match_id)
|
||||
} else {
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
.and_then(|run| run.focused_match())
|
||||
.and_then(|m| match m {
|
||||
BlockListMatch::RichContent { match_id, .. } => Some(*match_id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns find render data for a specific block, if find is active.
|
||||
///
|
||||
/// This works for both sync and async find paths.
|
||||
///
|
||||
/// Note: This method does NOT check if alt screen is active. It is intended
|
||||
/// for use during blocklist rendering where the caller has already determined
|
||||
/// that we are not in alt screen mode. Callers who need alt screen checking
|
||||
/// should do so before calling this method.
|
||||
///
|
||||
/// For async find, the grid handlers are needed to convert from absolute to
|
||||
/// relative coordinates and filter truncated matches.
|
||||
pub(crate) fn find_render_data_for_block(
|
||||
&self,
|
||||
block_index: BlockIndex,
|
||||
command_grid: Option<&GridHandler>,
|
||||
output_grid: Option<&GridHandler>,
|
||||
) -> Option<BlockFindRenderData<'_>> {
|
||||
if let Some(controller) = &self.async_find_controller {
|
||||
if !controller.has_active_find() {
|
||||
return None;
|
||||
}
|
||||
Some(BlockFindRenderData::from_async(
|
||||
controller,
|
||||
block_index,
|
||||
command_grid,
|
||||
output_grid,
|
||||
))
|
||||
} else {
|
||||
self.block_list_find_run
|
||||
.as_ref()
|
||||
.map(|run| BlockFindRenderData::from_sync(run, block_index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `FindOptions` applied to the active find run, if any.
|
||||
pub fn active_find_options(&self) -> Option<&FindOptions> {
|
||||
if self.terminal_model.lock().is_alt_screen_active() {
|
||||
self.alt_screen_find_run.as_ref().map(|run| run.options())
|
||||
} else if let Some(controller) = &self.async_find_controller {
|
||||
controller.find_options()
|
||||
} else {
|
||||
self.block_list_find_run.as_ref().map(|run| run.options())
|
||||
}
|
||||
@@ -134,22 +409,35 @@ impl TerminalFindModel {
|
||||
options,
|
||||
self.terminal_model.lock().alt_screen(),
|
||||
));
|
||||
} else {
|
||||
let _ = self.block_list_find_run.take();
|
||||
|
||||
let block_sort_direction = InputModeSettings::as_ref(ctx)
|
||||
.input_mode
|
||||
.value()
|
||||
.block_sort_direction();
|
||||
|
||||
self.block_list_find_run = Some(run_find_on_block_list(
|
||||
options,
|
||||
self.terminal_model.lock().block_list(),
|
||||
&self.rich_content_views,
|
||||
block_sort_direction,
|
||||
ctx,
|
||||
));
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
return;
|
||||
}
|
||||
|
||||
let block_sort_direction = InputModeSettings::as_ref(ctx)
|
||||
.input_mode
|
||||
.value()
|
||||
.block_sort_direction();
|
||||
|
||||
// Use async find if the feature flag is enabled.
|
||||
if let Some(controller) = &mut self.async_find_controller {
|
||||
log::trace!(
|
||||
"[async_find] Starting async find with query: {:?}",
|
||||
options.query
|
||||
);
|
||||
controller.start_find(&options, block_sort_direction, ctx);
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchronous path.
|
||||
let _ = self.block_list_find_run.take();
|
||||
self.block_list_find_run = Some(run_find_on_block_list(
|
||||
options,
|
||||
self.terminal_model.lock().block_list(),
|
||||
&self.rich_content_views,
|
||||
block_sort_direction,
|
||||
ctx,
|
||||
));
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
|
||||
@@ -162,37 +450,82 @@ impl TerminalFindModel {
|
||||
Some(old_find_state.rerun(self.terminal_model.lock().alt_screen()));
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
} else {
|
||||
// Find the last block index. This is the only block whose state may change.
|
||||
let last_block_index = self
|
||||
.terminal_model
|
||||
.lock()
|
||||
.block_list()
|
||||
.last_non_hidden_block_by_index()
|
||||
.unwrap_or_default();
|
||||
return;
|
||||
}
|
||||
|
||||
// Call find on the the last block's command and output grids.
|
||||
// If the block is a new finished block, the matches are inserted at a new key, the block's index in the blocklist.
|
||||
// If the block is an active, running block, its matches are overwritten in the terminal's block_matches.
|
||||
if let Some(block) = self
|
||||
.terminal_model
|
||||
.lock()
|
||||
.block_list()
|
||||
.block_at(last_block_index)
|
||||
{
|
||||
let block_sort_direction = InputModeSettings::as_ref(ctx)
|
||||
.input_mode
|
||||
.value()
|
||||
.block_sort_direction();
|
||||
// Handle async find path.
|
||||
if let Some(controller) = &self.async_find_controller {
|
||||
if !controller.has_active_find() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(old_find_run) = self.block_list_find_run.take() {
|
||||
self.block_list_find_run = Some(old_find_run.rerun_on_block(
|
||||
block,
|
||||
last_block_index,
|
||||
block_sort_direction,
|
||||
));
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
// Get the active block index and dirty range info.
|
||||
// We use active_block_index() (not last_non_hidden_block_by_index) because
|
||||
// the active block is where output is being written, even if it's still
|
||||
// "empty" and would be filtered out by the default BlockFilter.
|
||||
let mut model = self.terminal_model.lock();
|
||||
let active_block_index = model.block_list().active_block_index();
|
||||
|
||||
// Consume dirty ranges from both grids. We need mutable access
|
||||
// because take_find_dirty_rows_range is destructive.
|
||||
let active_block = model.block_list_mut().active_block_mut();
|
||||
let output_dirty_info =
|
||||
active_block
|
||||
.grid_of_type_mut(GridType::Output)
|
||||
.and_then(|grid| {
|
||||
let dirty = grid.grid_handler_mut().take_find_dirty_rows_range()?;
|
||||
let truncated = grid.grid_handler().num_lines_truncated();
|
||||
Some((dirty, GridType::Output, truncated))
|
||||
});
|
||||
let command_dirty_info = active_block
|
||||
.grid_of_type_mut(GridType::PromptAndCommand)
|
||||
.and_then(|grid| {
|
||||
let dirty = grid.grid_handler_mut().take_find_dirty_rows_range()?;
|
||||
let truncated = grid.grid_handler().num_lines_truncated();
|
||||
Some((dirty, GridType::PromptAndCommand, truncated))
|
||||
});
|
||||
|
||||
// Drop the model lock before emitting events.
|
||||
drop(model);
|
||||
|
||||
self.invalidate_async_find_block(active_block_index, output_dirty_info, ctx);
|
||||
if let Some(info) = command_dirty_info {
|
||||
self.invalidate_async_find_block(active_block_index, Some(info), ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync find path.
|
||||
// Find the last block index. This is the only block whose state may change.
|
||||
let last_block_index = self
|
||||
.terminal_model
|
||||
.lock()
|
||||
.block_list()
|
||||
.last_non_hidden_block_by_index()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Call find on the the last block's command and output grids.
|
||||
// If the block is a new finished block, the matches are inserted at a new key, the block's index in the blocklist.
|
||||
// If the block is an active, running block, its matches are overwritten in the terminal's block_matches.
|
||||
if let Some(block) = self
|
||||
.terminal_model
|
||||
.lock()
|
||||
.block_list()
|
||||
.block_at(last_block_index)
|
||||
{
|
||||
let block_sort_direction = InputModeSettings::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.input_mode
|
||||
.value()
|
||||
.block_sort_direction();
|
||||
|
||||
if let Some(old_find_run) = self.block_list_find_run.take() {
|
||||
self.block_list_find_run = Some(old_find_run.rerun_on_block(
|
||||
block,
|
||||
last_block_index,
|
||||
block_sort_direction,
|
||||
));
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +543,8 @@ impl TerminalFindModel {
|
||||
if let Some(alt_screen_find_run) = self.alt_screen_find_run.as_mut() {
|
||||
alt_screen_find_run.focus_next_match(find_direction);
|
||||
}
|
||||
} else if let Some(controller) = &mut self.async_find_controller {
|
||||
controller.focus_next_match(find_direction);
|
||||
} else if let Some(block_list_find_run) = self.block_list_find_run.as_mut() {
|
||||
let block_sort_direction = InputModeSettings::as_ref(ctx)
|
||||
.input_mode
|
||||
@@ -221,12 +556,31 @@ impl TerminalFindModel {
|
||||
ctx.emit(FindEvent::UpdatedFocusedMatch);
|
||||
}
|
||||
|
||||
/// Notifies every registered rich-content child view (e.g. AI blocks) to
|
||||
/// drop its cached find state and repaint, **without** touching the active
|
||||
/// find run's options/config.
|
||||
///
|
||||
/// Callers that just need stale highlights to disappear (e.g.
|
||||
/// `close_find_bar`) must use this rather than [`Self::clear_matches`].
|
||||
/// On the async path, `clear_matches` routes through
|
||||
/// `AsyncFindController::clear_results`, which also drops
|
||||
/// `current_find_options` — losing the query that `open_find_bar` later
|
||||
/// reads back via [`Self::active_find_options`] to restore the previous
|
||||
/// search.
|
||||
pub fn clear_rich_content_matches(&self, ctx: &mut ModelContext<Self>) {
|
||||
for view in self.rich_content_views.values() {
|
||||
view.clear_matches(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears matches in the active find run, if any.
|
||||
pub fn clear_matches(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if self.terminal_model.lock().is_alt_screen_active() {
|
||||
if let Some(run) = self.alt_screen_find_run.take() {
|
||||
self.alt_screen_find_run = Some(run.cleared());
|
||||
}
|
||||
} else if let Some(controller) = &mut self.async_find_controller {
|
||||
controller.clear_results(ctx);
|
||||
} else if let Some(run) = self.block_list_find_run.take() {
|
||||
for (_, rich_content_view) in self.rich_content_views.iter() {
|
||||
rich_content_view.clear_matches(ctx);
|
||||
@@ -247,6 +601,11 @@ impl TerminalFindModel {
|
||||
block_index: BlockIndex,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// Async find handles block invalidation differently via invalidate_block().
|
||||
if self.async_find_controller.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let terminal_model = self.terminal_model.lock();
|
||||
if let (Some(block_list_find_run), Some(filtered_block)) = (
|
||||
self.block_list_find_run.as_mut(),
|
||||
@@ -265,6 +624,93 @@ impl TerminalFindModel {
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if an async find operation is currently scanning.
|
||||
pub fn is_async_find_scanning(&self) -> bool {
|
||||
self.async_find_controller
|
||||
.as_ref()
|
||||
.map(|c| c.is_scanning())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Invalidates results for a specific block in async find.
|
||||
///
|
||||
/// This should be called when a block's content changes.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `block_index` - The index of the block that changed.
|
||||
/// * `dirty_info` - If provided, a `(row_range, grid_type, num_lines_truncated)`
|
||||
/// tuple describing the dirty region. If `None`, a full block rescan is enqueued.
|
||||
/// * `ctx` - The model context.
|
||||
pub fn invalidate_async_find_block(
|
||||
&mut self,
|
||||
block_index: BlockIndex,
|
||||
dirty_info: Option<(RangeInclusive<usize>, GridType, u64)>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(controller) = self.async_find_controller.as_mut() {
|
||||
controller.invalidate_block(block_index, dirty_info);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
ctx.emit(FindEvent::RanFind);
|
||||
}
|
||||
|
||||
/// Notifies async find that a block has completed.
|
||||
///
|
||||
/// This should be called when a command finishes, so that the completed block
|
||||
/// (which now has its final output) gets scanned for matches if find is active.
|
||||
/// Uses the dirty range accumulated during execution for incremental scanning.
|
||||
pub fn notify_block_completed(
|
||||
&mut self,
|
||||
block_index: BlockIndex,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.async_find_controller.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's an active find before acquiring the lock.
|
||||
let has_active_find = self
|
||||
.async_find_controller
|
||||
.as_ref()
|
||||
.map(|c| c.has_active_find())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_active_find {
|
||||
return;
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"[async_find] notify_block_completed: block_index={:?}",
|
||||
block_index
|
||||
);
|
||||
|
||||
// Get the dirty range from the completed block's output grid.
|
||||
// We need mutable access to consume the dirty range.
|
||||
let (dirty_range, num_lines_truncated) = {
|
||||
let mut model = self.terminal_model.lock();
|
||||
model
|
||||
.block_list_mut()
|
||||
.block_at_mut(block_index)
|
||||
.and_then(|block| block.grid_of_type_mut(GridType::Output))
|
||||
.map(|output_grid| {
|
||||
let dirty_range = output_grid.grid_handler_mut().take_find_dirty_rows_range();
|
||||
let num_lines_truncated = output_grid.grid_handler().num_lines_truncated();
|
||||
(dirty_range, num_lines_truncated)
|
||||
})
|
||||
.unwrap_or((None, 0))
|
||||
};
|
||||
|
||||
// Use invalidate_async_find_block which handles the dirty range properly.
|
||||
let dirty_info = dirty_range.map(|range| (range, GridType::Output, num_lines_truncated));
|
||||
self.invalidate_async_find_block(block_index, dirty_info, ctx);
|
||||
}
|
||||
|
||||
/// Returns the async find controller, if enabled.
|
||||
pub fn async_find_controller(&self) -> Option<&AsyncFindController> {
|
||||
self.async_find_controller.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TerminalFindModel {
|
||||
|
||||
Reference in New Issue
Block a user