1use anyhow::Result;
4use clap::Args as ClapArgs;
5use colored::Colorize;
6use regex::Regex;
7
8use crate::core::{AdrStatus, ConfigManager, Git, NotesManager};
9
10#[derive(ClapArgs, Debug)]
12pub struct Args {
13 pub query: String,
15
16 #[arg(long, short)]
18 pub status: Option<String>,
19
20 #[arg(long, short = 'g')]
22 pub tag: Option<String>,
23
24 #[arg(long, short)]
26 pub case_sensitive: bool,
27
28 #[arg(long, short = 'E')]
30 pub regex: bool,
31
32 #[arg(long, short = 'C', default_value = "2")]
34 pub context: usize,
35
36 #[arg(long)]
38 pub limit: Option<usize>,
39}
40
41struct SearchMatch {
43 line_number: usize,
44 line: String,
45 context_before: Vec<String>,
46 context_after: Vec<String>,
47}
48
49pub fn run(args: Args) -> Result<()> {
55 let git = Git::new();
56 git.check_repository()?;
57
58 let config = ConfigManager::new(git.clone()).load()?;
59 let notes = NotesManager::new(git, config);
60
61 let mut adrs = notes.list()?;
62
63 if let Some(status_str) = &args.status {
65 let status: AdrStatus = status_str.parse().map_err(|e| anyhow::anyhow!("{}", e))?;
66 adrs.retain(|a| a.frontmatter.status == status);
67 }
68
69 if let Some(tag) = &args.tag {
71 adrs.retain(|a| a.frontmatter.tags.iter().any(|t| t.contains(tag)));
72 }
73
74 let pattern = if args.regex {
76 if args.case_sensitive {
77 Regex::new(&args.query)?
78 } else {
79 Regex::new(&format!("(?i){}", &args.query))?
80 }
81 } else {
82 let escaped = regex::escape(&args.query);
83 if args.case_sensitive {
84 Regex::new(&escaped)?
85 } else {
86 Regex::new(&format!("(?i){}", escaped))?
87 }
88 };
89
90 let mut total_matches = 0;
91 let mut results = Vec::new();
92
93 for adr in &adrs {
94 let content = adr.to_markdown().unwrap_or_default();
95 let lines: Vec<&str> = content.lines().collect();
96 let mut matches = Vec::new();
97
98 for (idx, line) in lines.iter().enumerate() {
99 if pattern.is_match(line) {
100 let context_before: Vec<String> = lines[idx.saturating_sub(args.context)..idx]
102 .iter()
103 .map(|s| (*s).to_string())
104 .collect();
105
106 let context_after: Vec<String> = lines
107 [(idx + 1).min(lines.len())..(idx + 1 + args.context).min(lines.len())]
108 .iter()
109 .map(|s| (*s).to_string())
110 .collect();
111
112 matches.push(SearchMatch {
113 line_number: idx + 1,
114 line: (*line).to_string(),
115 context_before,
116 context_after,
117 });
118
119 total_matches += 1;
120 }
121 }
122
123 if !matches.is_empty() {
124 results.push((adr.clone(), matches));
125 }
126
127 if let Some(limit) = args.limit {
129 if results.len() >= limit {
130 break;
131 }
132 }
133 }
134
135 if results.is_empty() {
136 eprintln!("{} No matches found for: {}", "→".yellow(), args.query);
137 return Ok(());
138 }
139
140 for (adr, matches) in &results {
142 println!(
143 "{} {} - {}",
144 adr.id.cyan().bold(),
145 format!("[{}]", adr.frontmatter.status).dimmed(),
146 adr.frontmatter.title
147 );
148
149 for m in matches {
150 for ctx_line in &m.context_before {
152 println!(" {} {}", "│".dimmed(), ctx_line.dimmed());
153 }
154
155 let highlighted = pattern.replace_all(&m.line, |caps: ®ex::Captures| {
157 format!("{}", caps[0].red().bold())
158 });
159 println!(
160 " {} {}",
161 format!("{}:", m.line_number).yellow(),
162 highlighted
163 );
164
165 for ctx_line in &m.context_after {
167 println!(" {} {}", "│".dimmed(), ctx_line.dimmed());
168 }
169
170 println!();
171 }
172 }
173
174 eprintln!(
175 "{} {} match(es) in {} ADR(s)",
176 "→".blue(),
177 total_matches,
178 results.len()
179 );
180
181 Ok(())
182}