perf(tui/exec): reuse parsed metadata cache

This commit is contained in:
James Peter
2025-09-22 10:29:25 +10:00
parent 50399898a3
commit 9f2b39a0ce
+139 -44
View File
@@ -39,6 +39,7 @@ use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use shlex::Shlex;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::Cursor;
use std::path::Component;
use std::path::Path;
@@ -104,7 +105,7 @@ pub(crate) enum ExecAction {
}
pub(crate) fn action_enum_from_parsed(
parsed: &Vec<codex_core::parse_command::ParsedCommand>,
parsed: &[codex_core::parse_command::ParsedCommand],
) -> ExecAction {
use codex_core::parse_command::ParsedCommand;
for p in parsed {
@@ -805,7 +806,7 @@ impl ExploreAggregationCell {
session_root: &Path,
original_command: &[String],
) -> Option<usize> {
let action = action_enum_from_parsed(&parsed.to_vec());
let action = action_enum_from_parsed(parsed);
let summary = match action {
ExecAction::Search => parsed.iter().find_map(|p| match p {
ParsedCommand::Search { query, path, cmd } => {
@@ -1405,6 +1406,8 @@ pub(crate) struct ExecCell {
// Cached per-width layout (wrapped rows + totals) while content is stable
cached_layout: std::cell::RefCell<Option<Rc<ExecLayoutCache>>>,
cached_command_lines: std::cell::RefCell<Option<Vec<Line<'static>>>>,
cached_wait_extras: std::cell::RefCell<Option<Vec<Line<'static>>>>,
parsed_meta: Option<ParsedExecMetadata>,
has_bold_command: bool,
wait_state: std::cell::RefCell<ExecWaitState>,
}
@@ -1418,6 +1421,31 @@ struct ExecLayoutCache {
out_block_total: u16,
}
#[derive(Clone)]
struct ParsedExecMetadata {
action: ExecAction,
ctx_path: Option<String>,
search_paths: HashSet<String>,
}
impl ParsedExecMetadata {
fn from_commands(parsed: &[ParsedCommand]) -> Self {
let action = action_enum_from_parsed(parsed);
let ctx_path = first_context_path(parsed);
let mut search_paths: HashSet<String> = HashSet::new();
for pc in parsed {
if let ParsedCommand::Search { path: Some(p), .. } = pc {
search_paths.insert(p.to_string());
}
}
Self {
action,
ctx_path,
search_paths,
}
}
}
// ==================== AssistantMarkdownCell ====================
// Stores raw assistant markdown and rebuilds on demand (e.g., theme/syntax changes)
@@ -1831,7 +1859,7 @@ impl HistoryCell for ExecCell {
self
}
fn kind(&self) -> HistoryCellType {
let kind = match action_enum_from_parsed(&self.parsed) {
let kind = match self.parsed_action() {
ExecAction::Read => ExecKind::Read,
ExecAction::Search => ExecKind::Search,
ExecAction::List => ExecKind::List,
@@ -2019,8 +2047,18 @@ impl ExecCell {
self.cached_pre_lines.borrow_mut().take();
self.cached_out_lines.borrow_mut().take();
self.cached_layout.borrow_mut().take();
self.cached_wait_extras.borrow_mut().take();
}
fn parsed_action(&self) -> ExecAction {
self
.parsed_meta
.as_ref()
.map(|meta| meta.action)
.unwrap_or(ExecAction::Run)
}
pub(crate) fn set_waiting(&self, waiting: bool) {
let mut state = self.wait_state.borrow_mut();
if state.waiting != waiting {
@@ -2159,6 +2197,21 @@ impl ExecCell {
))
}
fn wait_extras(&self, state: &ExecWaitState) -> Vec<Line<'static>> {
if let Some(cached) = self.cached_wait_extras.borrow().as_ref() {
return cached.clone();
}
let mut extra_lines: Vec<Line<'static>> = Vec::new();
if let Some(summary_line) = self.wait_summary_line(state) {
extra_lines.push(summary_line);
}
extra_lines.extend(self.wait_note_lines(state));
if self.output.is_some() && !extra_lines.is_empty() {
*self.cached_wait_extras.borrow_mut() = Some(extra_lines.clone());
}
extra_lines
}
#[cfg(test)]
fn has_bold_command(&self) -> bool {
self.has_bold_command
@@ -2173,6 +2226,12 @@ impl ExecCell {
self.parsed = parsed;
self.has_bold_command = command_has_bold_token(&self.command);
self.cached_command_lines.borrow_mut().take();
self.cached_wait_extras.borrow_mut().take();
self.parsed_meta = if self.parsed.is_empty() {
None
} else {
Some(ParsedExecMetadata::from_commands(&self.parsed))
};
self.invalidate_render_caches();
}
/// Compute wrapped row totals for the preamble and the output at the given width.
@@ -2240,13 +2299,19 @@ impl ExecCell {
Vec<Line<'static>>,
Option<Line<'static>>,
) {
// For completed executions, cache pre/output segments since they are immutable.
if let (true, Some(pre), Some(out)) = (
self.output.is_some(),
if let (Some(pre), Some(out)) = (
self.cached_pre_lines.borrow().as_ref(),
self.cached_out_lines.borrow().as_ref(),
) {
return (pre.clone(), out.clone(), None);
if self.output.is_some() {
return (pre.clone(), out.clone(), None);
}
if self.stream_preview.is_some() {
let wait_state = self.wait_state_snapshot();
let status_label = if wait_state.waiting { "Waiting" } else { "Running" };
let status = self.streaming_status_line_for_label(status_label);
return (pre.clone(), out.clone(), status);
}
}
let wait_state = self.wait_state_snapshot();
@@ -2276,21 +2341,27 @@ impl ExecCell {
}
}
exec_render_parts_parsed(
&self.parsed,
self.output.as_ref(),
self.stream_preview.as_ref(),
self.start_time,
status_label,
)
match self.parsed_meta.as_ref() {
Some(meta) => exec_render_parts_parsed_with_meta(
&self.parsed,
meta,
self.output.as_ref(),
self.stream_preview.as_ref(),
self.start_time,
status_label,
),
None => exec_render_parts_parsed(
&self.parsed,
self.output.as_ref(),
self.stream_preview.as_ref(),
self.start_time,
status_label,
),
}
};
if self.output.is_some() {
let mut extra_lines: Vec<Line<'static>> = Vec::new();
if let Some(summary_line) = self.wait_summary_line(&wait_state) {
extra_lines.push(summary_line);
}
extra_lines.extend(self.wait_note_lines(&wait_state));
let extra_lines = self.wait_extras(&wait_state);
if !extra_lines.is_empty() {
let is_blank_line = |line: &Line<'static>| {
line.spans
@@ -2326,7 +2397,7 @@ impl ExecCell {
}
*self.cached_pre_lines.borrow_mut() = Some(pre.clone());
*self.cached_out_lines.borrow_mut() = Some(out.clone());
} else if self.parsed.is_empty() {
} else if self.output.is_none() {
*self.cached_pre_lines.borrow_mut() = Some(pre.clone());
*self.cached_out_lines.borrow_mut() = Some(out.clone());
}
@@ -2452,12 +2523,15 @@ impl ExecCell {
return Some(running_status_line(message));
}
let action = action_enum_from_parsed(&self.parsed.to_vec());
if !matches!(action, ExecAction::Run) {
let meta = match self.parsed_meta.as_ref() {
Some(meta) => meta,
None => return None,
};
if !matches!(meta.action, ExecAction::Run) {
return None;
}
let mut message = match first_context_path(&self.parsed) {
let mut message = match meta.ctx_path.as_deref() {
Some(p) => format!("{status_label}... in {p}"),
None => format!("{status_label}..."),
};
@@ -2626,7 +2700,7 @@ impl MergedExecCell {
}
pub(crate) fn from_exec(exec: &ExecCell) -> Self {
let (pre, out, _) = exec.exec_render_parts();
let kind = match action_enum_from_parsed(&exec.parsed) {
let kind = match exec.parsed_action() {
ExecAction::Read => ExecKind::Read,
ExecAction::Search => ExecKind::Search,
ExecAction::List => ExecKind::List,
@@ -3451,8 +3525,9 @@ impl HistoryCell for MergedExecCell {
}
}
fn exec_render_parts_parsed(
fn exec_render_parts_parsed_with_meta(
parsed_commands: &[ParsedCommand],
meta: &ParsedExecMetadata,
output: Option<&CommandOutput>,
stream_preview: Option<&CommandOutput>,
start_time: Option<Instant>,
@@ -3462,8 +3537,8 @@ fn exec_render_parts_parsed(
Vec<Line<'static>>,
Option<Line<'static>>,
) {
let action = action_enum_from_parsed(&parsed_commands.to_vec());
let ctx_path = first_context_path(parsed_commands);
let action = meta.action;
let ctx_path = meta.ctx_path.as_deref();
let suppress_run_header = matches!(action, ExecAction::Run) && output.is_some();
let mut pre: Vec<Line<'static>> = Vec::new();
let mut running_status: Option<Line<'static>> = None;
@@ -3552,12 +3627,7 @@ fn exec_render_parts_parsed(
}
// Reuse the same parsed-content rendering as new_parsed_command
let mut search_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
for pc in parsed_commands.iter() {
if let ParsedCommand::Search { path: Some(p), .. } = pc {
search_paths.insert(p.to_string());
}
}
let search_paths = &meta.search_paths;
// Compute output preview first to know whether to draw the downward corner.
let show_stdout = matches!(action, ExecAction::Run);
let display_output = output.or(stream_preview);
@@ -3882,6 +3952,28 @@ fn exec_render_parts_parsed(
(pre, out, running_status)
}
fn exec_render_parts_parsed(
parsed_commands: &[ParsedCommand],
output: Option<&CommandOutput>,
stream_preview: Option<&CommandOutput>,
start_time: Option<Instant>,
status_label: &str,
) -> (
Vec<Line<'static>>,
Vec<Line<'static>>,
Option<Line<'static>>,
) {
let meta = ParsedExecMetadata::from_commands(parsed_commands);
exec_render_parts_parsed_with_meta(
parsed_commands,
&meta,
output,
stream_preview,
start_time,
status_label,
)
}
// Local helper: coalesce "<file> (lines A to B)" entries when contiguous.
fn coalesce_read_ranges_in_lines_local(lines: &mut Vec<Line<'static>>) {
use ratatui::style::Modifier;
@@ -6367,6 +6459,11 @@ fn new_exec_cell(
None
};
let has_bold_command = command_has_bold_token(&command);
let parsed_meta = if parsed.is_empty() {
None
} else {
Some(ParsedExecMetadata::from_commands(&parsed))
};
ExecCell {
command,
parsed,
@@ -6378,6 +6475,8 @@ fn new_exec_cell(
cached_out_lines: std::cell::RefCell::new(None),
cached_layout: std::cell::RefCell::new(None),
cached_command_lines: std::cell::RefCell::new(None),
cached_wait_extras: std::cell::RefCell::new(None),
parsed_meta,
has_bold_command,
wait_state: std::cell::RefCell::new(ExecWaitState::default()),
}
@@ -7673,8 +7772,9 @@ fn new_parsed_command(
stream_preview: Option<&CommandOutput>,
start_time: Option<Instant>,
) -> Vec<Line<'static>> {
let action = action_enum_from_parsed(&parsed_commands.to_vec());
let ctx_path = first_context_path(parsed_commands);
let meta = ParsedExecMetadata::from_commands(parsed_commands);
let action = meta.action;
let ctx_path = meta.ctx_path.as_deref();
let suppress_run_header = matches!(action, ExecAction::Run) && output.is_some();
let mut lines: Vec<Line> = Vec::new();
let mut running_status: Option<Line<'static>> = None;
@@ -7725,7 +7825,7 @@ fn new_parsed_command(
Style::default().fg(crate::colors::text()),
));
} else {
let done = match &ctx_path {
let done = match ctx_path {
Some(p) => format!("Ran in {p}"),
None => "Ran".to_string(),
};
@@ -7752,7 +7852,7 @@ fn new_parsed_command(
Style::default().fg(crate::colors::text()),
));
} else {
let done = match &ctx_path {
let done = match ctx_path {
Some(p) => format!("Ran in {p}"),
None => "Ran".to_string(),
};
@@ -7768,12 +7868,7 @@ fn new_parsed_command(
}
// Collect any paths referenced by search commands to suppress redundant directory lines
let mut search_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
for pc in parsed_commands.iter() {
if let ParsedCommand::Search { path: Some(p), .. } = pc {
search_paths.insert(p.to_string());
}
}
let search_paths = &meta.search_paths;
// We'll emit only content lines here; the header above already communicates the action.
// Use a single leading "└ " for the very first content line, then indent subsequent ones,
@@ -8068,7 +8163,7 @@ fn new_parsed_command(
// the list path was suppressed because a Search referenced the same path),
// emit a single contextual line so the location is always visible.
if matches!(action, ExecAction::List) && !any_content_emitted {
let display_p = match &ctx_path {
let display_p = match ctx_path {
Some(p) if !p.is_empty() => {
if p.ends_with('/') {
p.to_string()