Skip to main content

binocular/search/
types.rs

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}