use super::*;
impl App {
pub fn search_in_diff_from_cursor(&mut self) -> bool {
let pattern = self.search_buffer.clone();
if pattern.trim().is_empty() {
self.set_message("Search pattern is empty");
return false;
}
self.last_search_pattern = Some(pattern.clone());
self.search_in_diff(&pattern, self.diff_state.cursor_line, true, true)
}
pub fn search_next_in_diff(&mut self) -> bool {
let Some(pattern) = self.last_search_pattern.clone() else {
self.set_message("No previous search");
return false;
};
self.search_in_diff(&pattern, self.diff_state.cursor_line, true, false)
}
pub fn search_prev_in_diff(&mut self) -> bool {
let Some(pattern) = self.last_search_pattern.clone() else {
self.set_message("No previous search");
return false;
};
self.search_in_diff(&pattern, self.diff_state.cursor_line, false, false)
}
fn search_in_diff(
&mut self,
pattern: &str,
start_idx: usize,
forward: bool,
include_current: bool,
) -> bool {
let total_lines = self.total_lines();
if total_lines == 0 {
self.set_message("No diff content to search");
return false;
}
if forward {
let mut idx = start_idx.min(total_lines.saturating_sub(1));
if !include_current {
idx = idx.saturating_add(1);
}
for line_idx in idx..total_lines {
if let Some(text) = self.line_text_for_search(line_idx)
&& text.contains(pattern)
{
self.diff_state.cursor_line = line_idx;
self.ensure_cursor_visible();
self.center_cursor();
self.update_current_file_from_cursor();
return true;
}
}
} else {
let mut idx = start_idx.min(total_lines.saturating_sub(1));
if !include_current {
idx = idx.saturating_sub(1);
}
let mut line_idx = idx;
loop {
if let Some(text) = self.line_text_for_search(line_idx)
&& text.contains(pattern)
{
self.diff_state.cursor_line = line_idx;
self.ensure_cursor_visible();
self.center_cursor();
self.update_current_file_from_cursor();
return true;
}
if line_idx == 0 {
break;
}
line_idx = line_idx.saturating_sub(1);
}
}
self.set_message(format!("No matches for \"{pattern}\""));
false
}
fn line_text_for_search(&self, line_idx: usize) -> Option<String> {
match self.line_annotations.get(line_idx)? {
AnnotatedLine::ReviewCommentsHeader => Some("Review comments".to_string()),
AnnotatedLine::ReviewComment { comment_idx } => {
let comment = self.session.review_comments.get(*comment_idx)?;
Some(comment.content.clone())
}
AnnotatedLine::RemoteReviewSummaryLine { summary_idx } => {
let summary = self.forge_review_summaries.get(*summary_idx)?;
let author = summary.author.as_deref().unwrap_or("unknown");
Some(format!("github @{author} {}", summary.body))
}
AnnotatedLine::FileHeader { file_idx } => {
let file = self.diff_files.get(*file_idx)?;
Some(format!(
"{} [{}]",
file.display_path().display(),
file.status.as_char()
))
}
AnnotatedLine::FileComment {
file_idx,
comment_idx,
} => {
let path = self.diff_files.get(*file_idx)?.display_path();
let review = self.session.files.get(path)?;
let comment = review.file_comments.get(*comment_idx)?;
Some(comment.content.clone())
}
AnnotatedLine::LineComment {
file_idx,
line,
comment_idx,
..
} => {
let path = self.diff_files.get(*file_idx)?.display_path();
let review = self.session.files.get(path)?;
let comments = review.line_comments.get(line)?;
let comment = comments.get(*comment_idx)?;
Some(comment.content.clone())
}
AnnotatedLine::Expander { gap_id, direction } => {
let arrow = match direction {
ExpandDirection::Down => "↓",
ExpandDirection::Up => "↑",
ExpandDirection::Both => "↕",
};
let gap = self.gap_size(gap_id)?;
let top_len = self.expanded_top.get(gap_id).map_or(0, |v| v.len());
let bot_len = self.expanded_bottom.get(gap_id).map_or(0, |v| v.len());
let remaining = (gap as usize).saturating_sub(top_len + bot_len);
let count = remaining.min(GAP_EXPAND_BATCH);
Some(format!("... {arrow} expand ({count} lines) ..."))
}
AnnotatedLine::HiddenLines { count, .. } => {
Some(format!("... {count} lines hidden ..."))
}
AnnotatedLine::ExpandedContext {
gap_id,
line_idx: context_idx,
} => {
let content = self.get_expanded_line(gap_id, *context_idx)?;
Some(content.content.clone())
}
AnnotatedLine::HunkHeader { file_idx, hunk_idx } => {
let file = self.diff_files.get(*file_idx)?;
let hunk = file.hunks.get(*hunk_idx)?;
Some(hunk.header.clone())
}
AnnotatedLine::DiffLine {
file_idx,
hunk_idx,
line_idx: diff_idx,
..
} => {
let file = self.diff_files.get(*file_idx)?;
let hunk = file.hunks.get(*hunk_idx)?;
let line = hunk.lines.get(*diff_idx)?;
Some(line.content.clone())
}
AnnotatedLine::BinaryOrEmpty { file_idx } => {
let file = self.diff_files.get(*file_idx)?;
if file.is_too_large {
Some("(file too large to display)".to_string())
} else if file.is_binary {
Some("(binary file)".to_string())
} else {
Some("(no changes)".to_string())
}
}
AnnotatedLine::SideBySideLine {
file_idx,
hunk_idx,
del_line_idx,
add_line_idx,
..
} => {
let file = self.diff_files.get(*file_idx)?;
let hunk = file.hunks.get(*hunk_idx)?;
let del_content = del_line_idx
.and_then(|idx| hunk.lines.get(idx))
.map(|l| l.content.as_str())
.unwrap_or("");
let add_content = add_line_idx
.and_then(|idx| hunk.lines.get(idx))
.map(|l| l.content.as_str())
.unwrap_or("");
Some(format!("{} {}", del_content, add_content))
}
AnnotatedLine::RemoteThreadLine { thread_idx } => {
let thread = self.forge_review_threads.get(*thread_idx)?;
let mut bodies: Vec<String> =
thread.comments.iter().map(|c| c.body.clone()).collect();
bodies.insert(0, format!("github {}", thread.path));
Some(bodies.join(" "))
}
AnnotatedLine::Spacing => None,
}
}
}