refactor(search): polish search results rendering with semantic Theme styles

Phase 5 of the UX overhaul. Migrates search result display from raw
console styling to the centralized Theme system with semantic methods,
improving visual consistency and readability.

Search result changes:
- Type badges now use semantic styles (issue_ref, mr_ref) with
  fixed-width alignment for clean columnar layout
- Snippet rendering uses Theme::highlight() for matched terms and
  Theme::muted() for surrounding context, replacing bold+underline
- Metadata line uses Theme::username() for authors and per-part
  styling with middle-dot separators instead of a single dim line
- Result numbering uses muted style with right-aligned width
- Consistent 8-space indent for metadata, snippets, and explain lines
- Header line uses muted style for search mode instead of dim+parens
- Trailing blank line moved after the result loop instead of per-result

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-14 09:59:09 -05:00
committed by teernisse
parent d710403567
commit 96b288ccdd

View File

@@ -309,20 +309,20 @@ fn parse_json_array(json: &str) -> Vec<String> {
.collect() .collect()
} }
/// Render FTS snippet with `<mark>` tags as terminal bold+underline. /// Render FTS snippet with `<mark>` tags as terminal highlight style.
fn render_snippet(snippet: &str) -> String { fn render_snippet(snippet: &str) -> String {
let mut result = String::new(); let mut result = String::new();
let mut remaining = snippet; let mut remaining = snippet;
while let Some(start) = remaining.find("<mark>") { while let Some(start) = remaining.find("<mark>") {
result.push_str(&remaining[..start]); result.push_str(&Theme::muted().render(&remaining[..start]));
remaining = &remaining[start + 6..]; remaining = &remaining[start + 6..];
if let Some(end) = remaining.find("</mark>") { if let Some(end) = remaining.find("</mark>") {
let highlighted = &remaining[..end]; let highlighted = &remaining[..end];
result.push_str(&Theme::bold().underline().render(highlighted)); result.push_str(&Theme::highlight().render(highlighted));
remaining = &remaining[end + 7..]; remaining = &remaining[end + 7..];
} }
} }
result.push_str(remaining); result.push_str(&Theme::muted().render(remaining));
result result
} }
@@ -345,32 +345,34 @@ pub fn print_search_results(response: &SearchResponse) {
"\n {} results for '{}' {}", "\n {} results for '{}' {}",
Theme::bold().render(&response.total_results.to_string()), Theme::bold().render(&response.total_results.to_string()),
Theme::bold().render(&response.query), Theme::bold().render(&response.query),
Theme::dim().render(&format!("({})", response.mode)) Theme::muted().render(&response.mode)
); );
println!();
for (i, result) in response.results.iter().enumerate() { for (i, result) in response.results.iter().enumerate() {
println!();
let type_badge = match result.source_type.as_str() { let type_badge = match result.source_type.as_str() {
"issue" => Theme::info().render("issue"), "issue" => Theme::issue_ref().render("issue"),
"merge_request" => Theme::accent().render("mr"), "merge_request" => Theme::mr_ref().render(" mr "),
"discussion" => Theme::info().render("disc"), "discussion" => Theme::info().render(" disc"),
"note" => Theme::info().render("note"), "note" => Theme::muted().render(" note"),
_ => Theme::dim().render(&result.source_type), _ => Theme::muted().render(&format!("{:>5}", &result.source_type)),
}; };
// Title line: rank, type badge, title // Title line: rank, type badge, title
println!( println!(
" {} {} {}", " {:>3}. {} {}",
Theme::dim().render(&format!("{:>2}.", i + 1)), Theme::muted().render(&(i + 1).to_string()),
type_badge, type_badge,
Theme::bold().render(&result.title) Theme::bold().render(&result.title)
); );
// Metadata: project, author, labels — compact middle-dot line // Metadata: project, author, labels — compact middle-dot line
let sep = Theme::muted().render(" \u{b7} ");
let mut meta_parts: Vec<String> = Vec::new(); let mut meta_parts: Vec<String> = Vec::new();
meta_parts.push(result.project_path.clone()); meta_parts.push(Theme::muted().render(&result.project_path));
if let Some(ref author) = result.author { if let Some(ref author) = result.author {
meta_parts.push(format!("@{author}")); meta_parts.push(Theme::username().render(&format!("@{author}")));
} }
if !result.labels.is_empty() { if !result.labels.is_empty() {
let label_str = if result.labels.len() <= 3 { let label_str = if result.labels.len() <= 3 {
@@ -382,16 +384,13 @@ pub fn print_search_results(response: &SearchResponse) {
result.labels.len() - 2 result.labels.len() - 2
) )
}; };
meta_parts.push(label_str); meta_parts.push(Theme::muted().render(&label_str));
} }
println!( println!(" {}", meta_parts.join(&sep));
" {}",
Theme::dim().render(&meta_parts.join(" \u{b7} "))
);
// Snippet with proper highlighting // Snippet with highlight styling
let rendered = render_snippet(&result.snippet); let rendered = render_snippet(&result.snippet);
println!(" {}", Theme::dim().render(&rendered)); println!(" {rendered}");
if let Some(ref explain) = result.explain { if let Some(ref explain) = result.explain {
println!( println!(
@@ -408,9 +407,9 @@ pub fn print_search_results(response: &SearchResponse) {
explain.rrf_score explain.rrf_score
); );
} }
}
println!(); println!();
}
} }
#[derive(Serialize)] #[derive(Serialize)]