1use crate::infra::channel::{self, Receiver, Sender};
2use crate::output::format_item_output;
3use crate::runtime::config::RunConfig;
4use crate::search::matcher::{spawn_exact_matcher, spawn_matcher, MatcherCommand, MatcherState};
5use crate::search::sources::{
6 spawn_git_searcher, spawn_searcher_with_config, spawn_stdin_searcher,
7};
8use crate::search::types::{SearchConfig, SearchItem, SearchResult};
9use std::io::{self, IsTerminal, Write};
10use std::sync::atomic::AtomicBool;
11use std::sync::Arc;
12
13pub fn run_with_configs(
14 run_config: RunConfig,
15 search_config: SearchConfig,
16 stdin_items: Option<Vec<String>>,
17) -> anyhow::Result<()> {
18 debug_assert!(run_config.headless);
19 run_with_search_config(search_config, stdin_items)
20}
21
22pub fn run_with_search_config(
23 search_config: SearchConfig,
24 stdin_items: Option<Vec<String>>,
25) -> anyhow::Result<()> {
26 let query = search_config
27 .query
28 .as_deref()
29 .unwrap_or("")
30 .trim()
31 .to_string();
32 if query.is_empty() {
33 stream_search_results(search_config, stdin_items)
34 } else {
35 stream_matched_results(search_config, stdin_items, query)
36 }
37}
38
39fn stream_search_results(
40 search_config: SearchConfig,
41 stdin_items: Option<Vec<String>>,
42) -> anyhow::Result<()> {
43 let (tx_items, rx_items) = channel::unbounded_default::<Vec<SearchItem>>();
44 let stop = Arc::new(AtomicBool::new(false));
45 if let Some(scope) = search_config.git_search_scope.clone() {
46 let _ = spawn_git_searcher(scope, stop, tx_items);
47 } else if let Some(items) = stdin_items {
48 let _ = spawn_stdin_searcher(items, stop, tx_items);
49 } else {
50 let _ = spawn_searcher_with_config(search_config, stop, tx_items);
51 }
52
53 let stdout = io::stdout();
54 let mut out = io::BufWriter::new(stdout.lock());
55 while let Ok(batch) = rx_items.recv() {
56 for item in batch {
57 writeln!(out, "{}", format_item_output(&item, None, false))?;
58 }
59 }
60
61 Ok(())
62}
63
64fn stream_matched_results(
65 search_config: SearchConfig,
66 stdin_items: Option<Vec<String>>,
67 query: String,
68) -> anyhow::Result<()> {
69 let (tx_items, rx_items) = channel::unbounded_default::<Vec<SearchItem>>();
70 let (tx_cmd, rx_cmd) = channel::unbounded_default::<MatcherCommand>();
71 let (tx_state, rx_state) = channel::unbounded_default::<MatcherState>();
72 let settings = search_config.settings;
73 let stop = Arc::new(AtomicBool::new(false));
74
75 if let Some(scope) = search_config.git_search_scope.clone() {
76 let _ = spawn_git_searcher(scope, stop.clone(), tx_items);
77 } else if let Some(items) = stdin_items {
78 let _ = spawn_stdin_searcher(items, stop.clone(), tx_items);
79 } else {
80 let _ = spawn_searcher_with_config(search_config, stop.clone(), tx_items);
81 }
82
83 if settings.matcher.is_exact() {
84 let _ = spawn_exact_matcher(
85 rx_items,
86 rx_cmd,
87 stop,
88 tx_state,
89 settings.mode.is_file_name_only(),
90 settings.mode.is_content(),
91 query.clone(),
92 );
93 } else {
94 let _ = spawn_matcher(
95 rx_items,
96 rx_cmd,
97 stop,
98 tx_state,
99 settings.mode.is_file_name_only(),
100 settings.mode.is_content(),
101 );
102 }
103 let _ = tx_cmd.send(MatcherCommand::Query(query));
104
105 loop {
106 match rx_state.recv() {
107 Ok(state) if !state.working => break,
108 Ok(_) => {}
109 Err(_) => return Ok(()),
110 }
111 }
112
113 while let Ok(Some(_)) = rx_state.try_recv() {}
114
115 let _ = tx_cmd.send(MatcherCommand::Resize(u32::MAX));
116 if let Ok(state) = rx_state.recv() {
117 write_match_results(&state.results, settings.mode.is_content())?;
118 }
119
120 Ok(())
121}
122
123fn write_match_results(results: &[SearchResult], is_content_mode: bool) -> anyhow::Result<()> {
124 let use_color = io::stdout().is_terminal();
125 let stdout = io::stdout();
126 let mut out = io::BufWriter::new(stdout.lock());
127
128 for result in results {
129 let line = if use_color && !result.indices.is_empty() {
130 format_colored_result(
131 &result.item,
132 &result.indices,
133 result.column,
134 is_content_mode,
135 )
136 } else {
137 format_item_output(&result.item, result.column, false)
138 };
139 writeln!(out, "{line}")?;
140 }
141
142 Ok(())
143}
144
145fn colorize_chars(text: &str, indices: &[u32]) -> String {
146 if indices.is_empty() {
147 return text.to_string();
148 }
149
150 let mut sorted: Vec<usize> = indices.iter().map(|&i| i as usize).collect();
151 sorted.sort_unstable();
152 sorted.dedup();
153
154 let mut result = String::with_capacity(text.len() + sorted.len() * 9);
155 let mut match_pos = 0;
156 let mut in_match = false;
157
158 for (char_idx, ch) in text.chars().enumerate() {
159 let is_match = match_pos < sorted.len() && sorted[match_pos] == char_idx;
160 if is_match {
161 match_pos += 1;
162 }
163 if is_match && !in_match {
164 result.push_str("\x1b[36m");
165 in_match = true;
166 } else if !is_match && in_match {
167 result.push_str("\x1b[0m");
168 in_match = false;
169 }
170 result.push(ch);
171 }
172
173 if in_match {
174 result.push_str("\x1b[0m");
175 }
176
177 result
178}
179
180fn format_colored_result(
181 item: &SearchItem,
182 indices: &[u32],
183 column: Option<usize>,
184 is_content_mode: bool,
185) -> String {
186 match item {
187 SearchItem::Stdin(text) | SearchItem::Message(text) => colorize_chars(text, indices),
188 SearchItem::Path(path) => colorize_chars(path, indices),
189 SearchItem::Grep { path, line, text } if is_content_mode => {
190 let line_num = line.to_string();
191 let content = text.trim_end_matches(['\n', '\r']);
192 let path_char_len = path.chars().count();
193 let prefix_char_len = path_char_len + 1 + line_num.chars().count() + 1;
194 let path_indices: Vec<u32> = indices
195 .iter()
196 .filter(|&&i| (i as usize) < path_char_len)
197 .copied()
198 .collect();
199 let content_indices: Vec<u32> = indices
200 .iter()
201 .filter_map(|&i| {
202 let i = i as usize;
203 if i >= prefix_char_len {
204 Some((i - prefix_char_len) as u32)
205 } else {
206 None
207 }
208 })
209 .collect();
210
211 let colored_path = colorize_chars(path, &path_indices);
212 let colored_content = colorize_chars(content, &content_indices);
213
214 if let Some(col) = column {
215 format!(
216 "{}:{}:\x1b[2m{}\x1b[0m:{}",
217 colored_path, line_num, col, colored_content
218 )
219 } else {
220 format!("{}:{}:{}", colored_path, line_num, colored_content)
221 }
222 }
223 SearchItem::GitHistory { .. }
224 | SearchItem::GitBranch { .. }
225 | SearchItem::GitCommit { .. } => format_item_output(item, column, false),
226 SearchItem::Grep { .. } => format_item_output(item, column, false),
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn colorize_chars_groups_consecutive_matches() {
236 assert_eq!(
237 colorize_chars("abcd", &[1, 2]),
238 "a\x1b[36mbc\x1b[0md".to_string()
239 );
240 }
241
242 #[test]
243 fn format_colored_result_offsets_grep_content_indices() {
244 let item = SearchItem::grep("src/main.rs", 7, "hello world");
245 let rendered = format_colored_result(&item, &[0, 1, 14, 15, 16], None, true);
246
247 assert!(rendered.contains("\x1b[36msr\x1b[0mc/main.rs"));
248 assert!(rendered.contains("\x1b[36mhel\x1b[0mlo world"));
249 }
250}