hyper_scripter/query/
util.rs

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