hyper_scripter/query/
util.rs

1use super::the_multifuzz_algo::{the_multifuzz_algo, MultiFuzzObj};
2use super::{ListQuery, ScriptQuery, ScriptQueryInner, StableRepo};
3use crate::color::Stylize;
4use crate::config::{Config, PromptLevel};
5use crate::error::{Error, Result};
6use crate::fuzzy;
7use crate::script_repo::{RepoEntry, Visibility};
8use crate::util::{get_display_type, prompt};
9use crate::Either;
10use crate::SEP;
11use fxhash::FxHashSet as HashSet;
12
13fn compute_vis(bang: bool) -> Visibility {
14    if bang {
15        Visibility::All
16    } else {
17        Visibility::Normal
18    }
19}
20
21pub async fn do_list_query<'a, R: StableRepo>(
22    repo: &'a mut R,
23    queries: impl IntoIterator<Item = ListQuery>,
24) -> Result<Vec<RepoEntry<'a>>> {
25    do_list_query_with_handler(repo, queries, &mut super::DefaultListQueryHandler).await
26}
27
28pub async fn do_list_query_with_handler<
29    'a,
30    R: StableRepo,
31    I,
32    H: super::ListQueryHandler<Item = I>,
33>(
34    repo: &'a mut R,
35    queries: impl IntoIterator<Item = I>,
36    handler: &mut H,
37) -> Result<Vec<RepoEntry<'a>>> {
38    let mut is_empty = true;
39    let mut mem = HashSet::<i64>::default();
40    let mut ret = vec![];
41    let repo_ptr = repo as *mut R;
42    for query in queries {
43        is_empty = false;
44        macro_rules! insert {
45            ($script:ident) => {
46                if mem.contains(&$script.id) {
47                    continue;
48                }
49                mem.insert($script.id);
50                ret.push($script);
51            };
52        }
53        // SAFETY: `mem` 已保證回傳的陣列不可能包含相同的資料,且 `StableRepo` 保證了內容物不會位移
54        let repo = unsafe { &mut *repo_ptr };
55        let query = handler.handle_item(query);
56        match query {
57            None => (),
58            Some(ListQuery::Pattern(re, og, bang)) => {
59                let mut is_empty = true;
60                for script in repo.iter_mut(compute_vis(bang)) {
61                    if re.is_match(&script.name.key()) {
62                        is_empty = false;
63                        insert!(script);
64                    }
65                }
66                if is_empty {
67                    return Err(Error::ScriptNotFound(og.to_owned()));
68                }
69            }
70            Some(ListQuery::Query(query)) => {
71                let script = match handler.handle_query(query, repo).await {
72                    Ok(Some(entry)) => entry,
73                    Ok(None) => continue,
74                    Err(e) => return Err(e),
75                };
76                insert!(script);
77            }
78        }
79    }
80    if is_empty && H::should_return_all_on_empty() {
81        return Ok(repo.iter_mut(Visibility::Normal).collect());
82    }
83    if ret.is_empty() && H::should_raise_dont_fuzz_on_empty() {
84        log::debug!("列表查不到東西,卻又不是因為 pattern not match,想必是因為使用者取消了模糊搜");
85        Err(Error::DontFuzz)
86    } else {
87        Ok(ret)
88    }
89}
90
91impl<'a> MultiFuzzObj for RepoEntry<'a> {
92    fn beats(&self, other: &Self) -> bool {
93        self.last_time() > other.last_time()
94    }
95}
96
97pub async fn do_script_query<'b, R: StableRepo>(
98    script_query: &ScriptQuery,
99    script_repo: &'b mut R,
100    finding_filtered: bool,
101    forbid_prompt: bool,
102) -> Result<Option<RepoEntry<'b>>> {
103    log::debug!("開始尋找 `{:?}`", script_query);
104    let mut visibility = compute_vis(script_query.bang);
105    if finding_filtered {
106        visibility = visibility.invert();
107    }
108    match &script_query.inner {
109        ScriptQueryInner::Prev(prev) => {
110            assert!(!finding_filtered); // XXX 很難看的作法,應設法靜態檢查
111            let latest = script_repo.latest_mut(prev.get(), visibility);
112            log::trace!("找最新腳本");
113            return if latest.is_some() {
114                Ok(latest)
115            } else {
116                Err(Error::Empty)
117            };
118        }
119        ScriptQueryInner::Exact(name) => Ok(script_repo.get_mut(name, visibility)),
120        ScriptQueryInner::Fuzz(name) => {
121            let level = if forbid_prompt {
122                PromptLevel::Never
123            } else {
124                Config::get_prompt_level()
125            };
126
127            let iter = script_repo.iter_mut(visibility);
128            let fuzz_res = fuzzy::fuzz(name, iter, SEP).await?;
129            let mut is_low = false;
130            let mut is_multi_fuzz = false;
131            let entry = match fuzz_res {
132                Some(fuzzy::High(entry)) => entry,
133                Some(fuzzy::Low(entry)) => {
134                    is_low = true;
135                    entry
136                }
137                #[cfg(feature = "benching")]
138                Some(fuzzy::Multi { ans, .. }) => {
139                    is_multi_fuzz = true;
140                    ans
141                }
142                #[cfg(not(feature = "benching"))]
143                Some(fuzzy::Multi { ans, others, .. }) => {
144                    is_multi_fuzz = true;
145                    match handle_special_dot_anonymous(name, ans, others) {
146                        Either::One(res) => res,
147                        Either::Two((ans, others)) => the_multifuzz_algo(ans, others),
148                    }
149                }
150                None => return Ok(None),
151            };
152            let need_prompt = {
153                match level {
154                    PromptLevel::Always => true,
155                    PromptLevel::Never => false,
156                    PromptLevel::Smart => is_low || is_multi_fuzz,
157                    PromptLevel::OnMultiFuzz => is_multi_fuzz,
158                }
159            };
160            if need_prompt {
161                let ty = get_display_type(&entry.ty);
162                let msg = format!("{}({})?", entry.name, ty.display());
163                let yes = prompt(msg.stylize().color(ty.color()).bold(), true)?;
164                if !yes {
165                    return Err(Error::DontFuzz);
166                }
167            }
168            Ok(Some(entry))
169        }
170    }
171}
172pub async fn do_script_query_strict<'b, R: StableRepo>(
173    script_query: &ScriptQuery,
174    script_repo: &'b mut R,
175) -> Result<RepoEntry<'b>> {
176    // FIXME: 一旦 NLL 進化就修掉這段 unsafe
177    let ptr = script_repo as *mut R;
178    if let Some(info) = do_script_query(script_query, script_repo, false, false).await? {
179        return Ok(info);
180    }
181
182    let script_repo = unsafe { &mut *ptr };
183    #[cfg(not(feature = "benching"))]
184    if !script_query.bang {
185        let filtered = do_script_query(script_query, script_repo, true, true).await?;
186        if let Some(filtered) = filtered {
187            return Err(Error::ScriptIsFiltered(filtered.name.key().to_string()));
188        }
189    };
190
191    Err(Error::ScriptNotFound(script_query.to_string()))
192}
193
194// 判斷特例:若收到的查詢是單一個 ".",且所有候選人皆為匿名,則不再考慮任何前綴問題
195// 舉例:若有兩個腳本 .1 和 .10,用 "." 來查詢不該讓 .1 遮蔽掉 .10
196// (但若是用 "1" 來查就該遮蔽)
197fn handle_special_dot_anonymous<'a>(
198    pattern: &str,
199    ans: RepoEntry<'a>,
200    others: Vec<RepoEntry<'a>>,
201) -> Either<RepoEntry<'a>, (RepoEntry<'a>, Vec<RepoEntry<'a>>)> {
202    if pattern != "." {
203        return Either::Two((ans, others));
204    }
205    // TODO: maybe only check `ans` is enough?
206    if !ans.name.is_anonymous() || others.iter().any(|e| !e.name.is_anonymous()) {
207        return Either::Two((ans, others));
208    }
209
210    let res = std::iter::once(ans)
211        .chain(others.into_iter())
212        .max_by_key(|e| e.last_time())
213        .unwrap();
214    Either::One(res)
215}