1use crate::cli::args::Args;
2use crate::search::sources::git::HISTORY_PATH_SEPARATOR;
3use std::borrow::Cow;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SearchMode {
8 Path,
9 Files,
10 Grep,
11 Dirs,
12 GitHistory,
13 GitBranches,
14 GitCommits,
15}
16
17impl SearchMode {
18 pub fn from_args(args: &Args) -> Self {
19 if args.git_history.is_some() {
20 Self::GitHistory
21 } else if args.git_branches {
22 Self::GitBranches
23 } else if args.git_commits {
24 Self::GitCommits
25 } else if args.content {
26 Self::Grep
27 } else if args.dir_only {
28 Self::Dirs
29 } else if args.file_name {
30 Self::Files
31 } else {
32 Self::Path
33 }
34 }
35
36 pub fn is_content(self) -> bool {
37 matches!(self, Self::Grep | Self::GitHistory)
38 }
39
40 pub fn is_dir_only(self) -> bool {
41 matches!(self, Self::Dirs)
42 }
43
44 pub fn is_file_name_only(self) -> bool {
45 matches!(self, Self::Files)
46 }
47
48 pub fn display_name(self, stdin: bool) -> &'static str {
49 if stdin {
50 "Stdin"
51 } else {
52 match self {
53 Self::Path => "Path",
54 Self::Files => "Files",
55 Self::Grep => "Grep",
56 Self::Dirs => "Dirs",
57 Self::GitHistory => "History",
58 Self::GitBranches => "Branches",
59 Self::GitCommits => "Commits",
60 }
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MatcherMode {
67 Fuzzy,
68 Exact,
69}
70
71impl MatcherMode {
72 pub fn from_args(args: &Args) -> Self {
73 if args.exact {
74 Self::Exact
75 } else {
76 Self::Fuzzy
77 }
78 }
79
80 pub fn is_exact(self) -> bool {
81 matches!(self, Self::Exact)
82 }
83
84 pub fn toggle(self) -> Self {
85 match self {
86 Self::Fuzzy => Self::Exact,
87 Self::Exact => Self::Fuzzy,
88 }
89 }
90
91 pub fn display_name(self) -> &'static str {
92 match self {
93 Self::Fuzzy => "Fuzzy",
94 Self::Exact => "Exact",
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct SearchSettings {
101 pub mode: SearchMode,
102 pub matcher: MatcherMode,
103}
104
105impl SearchSettings {
106 pub fn from_args(args: &Args) -> Self {
107 Self {
108 mode: SearchMode::from_args(args),
109 matcher: MatcherMode::from_args(args),
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct SearchConfig {
116 pub query: Option<String>,
117 pub locations: Vec<PathBuf>,
118 pub search_pdf: bool,
119 pub no_hidden: bool,
120 pub no_git_ignore: bool,
121 pub no_ignore: bool,
122 pub no_default_ignore_dirs: bool,
123 pub git_search_scope: Option<crate::search::sources::git::GitSearchScope>,
124 pub settings: SearchSettings,
125}
126
127impl SearchConfig {
128 pub fn from_args(args: &Args) -> Self {
129 Self {
130 query: args.query.clone(),
131 locations: args.location.clone(),
132 search_pdf: args.search_pdf,
133 no_hidden: args.no_hidden,
134 no_git_ignore: args.no_git_ignore,
135 no_ignore: args.no_ignore,
136 no_default_ignore_dirs: args.no_default_ignore_dirs,
137 git_search_scope: args.git_search_scope.clone(),
138 settings: SearchSettings::from_args(args),
139 }
140 }
141
142 pub fn with_settings(&self, settings: SearchSettings) -> Self {
143 let mut config = self.clone();
144 config.settings = settings;
145 config
146 }
147
148 pub fn with_git_search_scope(
149 &self,
150 git_search_scope: Option<crate::search::sources::git::GitSearchScope>,
151 ) -> Self {
152 let mut config = self.clone();
153 config.git_search_scope = git_search_scope;
154 config
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash)]
159pub enum SearchItem {
160 Path(String),
161 Grep {
162 path: String,
163 line: usize,
164 text: String,
165 },
166 GitHistory {
167 commit: String,
168 path: String,
169 line: usize,
170 text: String,
171 },
172 GitBranch {
173 branch: String,
174 commit: String,
175 subject: String,
176 is_head: bool,
177 relative_time: String,
178 },
179 GitCommit {
180 commit: String,
181 short_commit: String,
182 subject: String,
183 author: String,
184 date: String,
185 refs: String,
186 },
187 Stdin(String),
188 Message(String),
189}
190
191impl SearchItem {
192 pub fn path(path: impl Into<String>) -> Self {
193 Self::Path(path.into())
194 }
195
196 pub fn grep(path: impl Into<String>, line: usize, text: impl Into<String>) -> Self {
197 Self::Grep {
198 path: path.into(),
199 line,
200 text: text.into(),
201 }
202 }
203
204 pub fn stdin(text: impl Into<String>) -> Self {
205 Self::Stdin(text.into())
206 }
207
208 pub fn history_line(
209 commit: impl Into<String>,
210 path: impl Into<String>,
211 line: usize,
212 text: impl Into<String>,
213 ) -> Self {
214 Self::GitHistory {
215 commit: commit.into(),
216 path: path.into(),
217 line,
218 text: text.into(),
219 }
220 }
221
222 pub fn history_error(message: impl Into<String>) -> Self {
223 Self::Message(message.into())
224 }
225
226 pub fn message(text: impl Into<String>) -> Self {
227 Self::Message(text.into())
228 }
229
230 pub fn git_branch(
231 branch: impl Into<String>,
232 commit: impl Into<String>,
233 subject: impl Into<String>,
234 is_head: bool,
235 relative_time: impl Into<String>,
236 ) -> Self {
237 Self::GitBranch {
238 branch: branch.into(),
239 commit: commit.into(),
240 subject: subject.into(),
241 is_head,
242 relative_time: relative_time.into(),
243 }
244 }
245
246 pub fn git_commit(
247 commit: impl Into<String>,
248 short_commit: impl Into<String>,
249 subject: impl Into<String>,
250 author: impl Into<String>,
251 date: impl Into<String>,
252 refs: impl Into<String>,
253 ) -> Self {
254 Self::GitCommit {
255 commit: commit.into(),
256 short_commit: short_commit.into(),
257 subject: subject.into(),
258 author: author.into(),
259 date: date.into(),
260 refs: refs.into(),
261 }
262 }
263
264 pub fn match_text(&self, use_filename_only: bool) -> Cow<'_, str> {
265 match self {
266 Self::Path(path) => {
267 if use_filename_only {
268 std::path::Path::new(path)
269 .file_name()
270 .and_then(|n| n.to_str())
271 .map(Cow::Borrowed)
272 .unwrap_or_else(|| Cow::Borrowed(path.as_str()))
273 } else {
274 Cow::Borrowed(path.as_str())
275 }
276 }
277 Self::Grep { path, line, text } => {
278 if use_filename_only {
279 std::path::Path::new(path)
280 .file_name()
281 .and_then(|n| n.to_str())
282 .map(Cow::Borrowed)
283 .unwrap_or_else(|| Cow::Borrowed(path.as_str()))
284 } else {
285 Cow::Owned(format!("{path}:{line}:{text}"))
286 }
287 }
288 Self::GitHistory {
289 commit,
290 path,
291 line,
292 text,
293 } => {
294 if use_filename_only {
295 std::path::Path::new(path)
296 .file_name()
297 .and_then(|n| n.to_str())
298 .map(Cow::Borrowed)
299 .unwrap_or_else(|| Cow::Borrowed(path.as_str()))
300 } else {
301 Cow::Owned(format!("{commit} {path}:{line}:{text}"))
302 }
303 }
304 Self::GitBranch {
305 branch,
306 commit,
307 subject,
308 is_head,
309 relative_time,
310 } => {
311 let head = if *is_head { "HEAD " } else { "" };
312 Cow::Owned(format!("{head}{branch} {commit} {subject} {relative_time}"))
313 }
314 Self::GitCommit {
315 short_commit,
316 subject,
317 author,
318 date,
319 refs,
320 ..
321 } => Cow::Owned(format!("{short_commit} {refs} {subject} {date} {author}")),
322 Self::Stdin(text) => Cow::Borrowed(text.as_str()),
323 Self::Message(text) => Cow::Borrowed(text.as_str()),
324 }
325 }
326
327 pub fn display_text(&self) -> Cow<'_, str> {
328 match self {
329 Self::Path(path) => Cow::Borrowed(path.as_str()),
330 Self::Grep { path, line, text } => Cow::Owned(format!("{path}:{line}:{text}")),
331 Self::GitHistory {
332 commit,
333 path,
334 line,
335 text,
336 } => Cow::Owned(format!("{commit}: {path}:{line}:{text}")),
337 Self::GitBranch { branch, .. } => Cow::Borrowed(branch.as_str()),
338 Self::GitCommit {
339 short_commit,
340 subject,
341 author,
342 date,
343 refs,
344 ..
345 } => {
346 let refs = refs.trim();
347 if refs.is_empty() {
348 Cow::Owned(format!("[{short_commit}] - {subject} ({date}) <{author}>"))
349 } else {
350 Cow::Owned(format!(
351 "[{short_commit}] - ({refs}) {subject} ({date}) <{author}>"
352 ))
353 }
354 }
355 Self::Stdin(text) => Cow::Borrowed(text.as_str()),
356 Self::Message(text) => Cow::Borrowed(text.as_str()),
357 }
358 }
359
360 pub fn preview_path(&self) -> Option<&str> {
361 match self {
362 Self::Path(path) | Self::Grep { path, .. } | Self::GitHistory { path, .. } => {
363 Some(path.as_str())
364 }
365 Self::GitBranch { .. } | Self::GitCommit { .. } | Self::Stdin(_) | Self::Message(_) => {
366 None
367 }
368 }
369 }
370
371 pub fn grep_line(&self) -> Option<usize> {
372 match self {
373 Self::Grep { line, .. } | Self::GitHistory { line, .. } => Some(*line),
374 _ => None,
375 }
376 }
377
378 pub fn git_history_commit(&self) -> Option<&str> {
379 match self {
380 Self::GitHistory { commit, .. } => Some(commit.as_str()),
381 Self::GitCommit { commit, .. } => Some(commit.as_str()),
382 _ => None,
383 }
384 }
385
386 pub fn git_branch_name(&self) -> Option<&str> {
387 match self {
388 Self::GitBranch { branch, .. } => Some(branch.as_str()),
389 _ => None,
390 }
391 }
392
393 pub fn git_branch_is_head(&self) -> bool {
394 matches!(self, Self::GitBranch { is_head: true, .. })
395 }
396
397 pub fn is_content_search_item(&self) -> bool {
398 matches!(self, Self::Grep { .. } | Self::GitHistory { .. })
399 }
400
401 pub fn is_stdin(&self) -> bool {
402 matches!(self, Self::Stdin(_))
403 }
404
405 pub fn content_match_column(&self, match_indices: &[u32]) -> Option<usize> {
406 match self {
407 Self::Grep { .. } => crate::text::find_first_match_column_in_grep_result(
408 self.display_text().as_ref(),
409 match_indices,
410 ),
411 Self::GitHistory {
412 commit, path, line, ..
413 } => {
414 let prefix = format!(
415 "{}: {}:{}:",
416 commit,
417 path.replace(HISTORY_PATH_SEPARATOR, "/"),
418 line
419 );
420 match_indices
421 .iter()
422 .find(|&&idx| idx as usize >= prefix.chars().count())
423 .map(|idx| (*idx as usize) - prefix.chars().count() + 1)
424 }
425 _ => None,
426 }
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct SearchResult {
432 pub item: SearchItem,
433 pub indices: Vec<u32>,
434 pub column: Option<usize>,
435}