flams_router_search/
vscode.rs

1use std::fmt::Write;
2
3use crate::components::SearchState;
4use flams_ontology::{
5    search::{QueryFilter, SearchResult, SearchResultKind},
6    uris::{
7        ArchiveId, ArchiveURITrait, ContentURITrait, DocumentElementURI, DocumentURI, NarrativeURI,
8        PathURITrait, SymbolURI, URI,
9    },
10};
11use flams_router_base::uris::{URIComponents, URIComponentsTrait};
12use flams_router_vscode::{
13    VSCode,
14    components::{VSCodeButton, VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup, VSCodeTextbox},
15};
16use flams_utils::{impossible, unwrap};
17use flams_web_utils::{components::wait_and_then_fn, do_css, inject_css};
18use ftml_viewer_components::components::omdoc::{comma_sep, doc_name, symbol_name};
19use leptos::prelude::*;
20
21#[component]
22pub fn VSCodeSearch() -> impl IntoView {
23    inject_css("flams-search-block", include_str!("vscode.css"));
24    use flams_web_utils::components::Themer;
25    use ftml_viewer_components::FTMLGlobalSetup;
26
27    let remote = || leptos_router::hooks::use_query_map().with(|q| q.get_string("remote"));
28
29    let selected_radio = RwSignal::new(Some("doc".to_string()));
30    let disabled =
31        Memo::new(move |_| selected_radio.with(|s| s.as_ref().is_some_and(|s| s == "symbol")));
32
33    let full_docs = RwSignal::new(false);
34    let paras = RwSignal::new(true);
35    let defs = RwSignal::new(true);
36    let exs = RwSignal::new(true);
37    let asss = RwSignal::new(false);
38    let probs = RwSignal::new(false);
39    let query = RwSignal::new(String::default());
40    let opts = Memo::new(move |_| {
41        let mut ret = QueryFilter::default();
42        ret.allow_documents = full_docs.get();
43        ret.allow_paragraphs = paras.get();
44        ret.allow_definitions = defs.get();
45        ret.allow_examples = exs.get();
46        ret.allow_assertions = asss.get();
47        ret.allow_problems = probs.get();
48        ret
49    });
50    let local_results = RwSignal::new(SearchState::None);
51    let remote_results = RwSignal::new(SearchState::None);
52    let local_act = Action::new(move |&()| {
53        let query = query.get_untracked();
54        local_results.set(SearchState::Loading);
55        let opts = opts.get_untracked();
56        async move {
57            match super::search_query(query, opts, 20).await {
58                Ok(r) => local_results.set(SearchState::Results(r)),
59                Err(_) => {
60                    local_results.set(SearchState::None);
61                }
62            }
63        }
64    });
65    let remote_act = Action::new(move |&()| {
66        let remote = remote();
67        let query = query.get_untracked();
68        remote_results.set(SearchState::Loading);
69        let opts = opts.get_untracked();
70        async move {
71            let Some(remote) = remote else { return };
72            #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
73            {
74                use flams_router_base::ServerFnExt;
75                let query = super::SearchQuery {
76                    query,
77                    opts,
78                    num_results: 20,
79                }
80                .call_remote(remote)
81                .await;
82                match query {
83                    Ok(r) => remote_results.set(SearchState::Results(r)),
84                    Err(_) => {
85                        remote_results.set(SearchState::None);
86                    }
87                }
88            }
89        }
90    });
91    let local_sym_act = Action::new(move |&()| {
92        let query = query.get_untracked();
93        local_results.set(SearchState::Loading);
94        async move {
95            match super::search_symbols(query, 20).await {
96                Ok(r) => local_results.set(SearchState::SymResults(r)),
97                Err(_) => {
98                    local_results.set(SearchState::None);
99                }
100            }
101        }
102    });
103    let remote_sym_act = Action::new(move |&()| {
104        let remote = remote();
105        let query = query.get_untracked();
106        remote_results.set(SearchState::Loading);
107        async move {
108            let Some(remote) = remote else { return };
109            #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
110            {
111                use flams_router_base::ServerFnExt;
112                let query = super::SearchSymbols {
113                    query,
114                    num_results: 20,
115                }
116                .call_remote(remote)
117                .await;
118                match query {
119                    Ok(r) => remote_results.set(SearchState::SymResults(r)),
120                    Err(_) => {
121                        remote_results.set(SearchState::None);
122                    }
123                };
124            }
125        }
126    });
127    Effect::new(move || {
128        if query.with(String::is_empty) {
129            local_results.set(SearchState::None);
130            return;
131        }
132        if selected_radio.with(|v| v.as_ref().is_some_and(|s| s == "symbol")) {
133            local_sym_act.dispatch(());
134            remote_sym_act.dispatch(());
135        } else {
136            let _ = opts.get();
137            local_act.dispatch(());
138            remote_act.dispatch(());
139        }
140    });
141
142    view! {
143        <div style="display:flex;flex-direction:column;">
144            <VSCodeTextbox value=query placeholder="Search"/>
145            <VSCodeRadioGroup name="flams-vscode-search" selected=selected_radio>
146                <div style="display:flex;flex-direction:row;">
147                    <VSCodeRadio id="symbol">"Symbols"</VSCodeRadio>
148                    <VSCodeRadio id="doc">"Paragraphs"</VSCodeRadio>
149                </div>
150            </VSCodeRadioGroup>
151            <div style="display:flex;flex-direction:row;flex-wrap:wrap;">
152                <VSCodeCheckbox checked=full_docs disabled>"Full Documents"</VSCodeCheckbox>
153                <VSCodeCheckbox checked=paras disabled>"Paragraphs"</VSCodeCheckbox>
154                <VSCodeCheckbox checked=defs disabled>"Definitions"</VSCodeCheckbox>
155                <VSCodeCheckbox checked=exs disabled>"Examples"</VSCodeCheckbox>
156                <VSCodeCheckbox checked=asss disabled>"Assertions"</VSCodeCheckbox>
157                <VSCodeCheckbox checked=probs disabled>"Problems"</VSCodeCheckbox>
158                <Themer><FTMLGlobalSetup>
159                {do_results("Local Results",None,local_results)}
160                <div style="margin-top:25px;"></div>
161                {do_results("Remote Results",Some(remote),remote_results)}
162                </FTMLGlobalSetup></Themer>
163            </div>
164        </div>
165    }
166}
167
168fn do_results(
169    pre: &'static str,
170    remote: Option<fn() -> Option<String>>,
171    results: RwSignal<SearchState>,
172) -> impl IntoView {
173    use leptos::either::EitherOf6::*;
174    inject_css(
175        "ftml-comp",
176        include_str!("../../../ftml/viewer-components/src/components/comp.css"),
177    );
178    let pre_view =
179        move || view! {<div style="width:100%;font-weight:bold;text-align:center;">{pre}</div>};
180    move || {
181        results.with(|r| match r {
182            SearchState::None => A(()),
183            SearchState::Results(v) if v.is_empty() => B(view!({pre_view}"(No results)")),
184            SearchState::Loading => C(view!({pre_view}<flams_web_utils::components::Spinner/>)),
185            SearchState::SymResults(v) if remote.is_none() => D(view!({pre_view}{v
186            .iter()
187            .map(|(sym, _)| do_sym_result_local(sym))
188            .collect_view()})),
189            SearchState::SymResults(v) => E(view!({pre_view}{v
190            .iter()
191            .map(|(sym, res)| do_sym_result_remote(sym, res.clone(),unwrap!(remote)))
192            .collect_view()})),
193            SearchState::Results(v) => F(view!({pre_view}{v
194            .iter()
195            .map(|(score, res)| do_result(*score, res,remote))
196            .collect_view()})),
197        })
198    }
199}
200
201fn do_result(
202    score: f32,
203    res: &SearchResult,
204    remote: Option<fn() -> Option<String>>,
205) -> impl IntoView + use<> {
206    use leptos::either::Either::*;
207    match res {
208        SearchResult::Document(d) => Left(do_doc(score, d.clone(), remote)),
209        SearchResult::Paragraph {
210            uri, fors, kind, ..
211        } => Right(do_para(score, uri.clone(), *kind, fors.clone(), remote)),
212    }
213}
214
215#[derive(leptos::server_fn::serde::Serialize, Debug, Clone)]
216struct Usemodule {
217    kind: &'static str,
218    archive: ArchiveId,
219    path: String,
220}
221impl Usemodule {
222    fn make(uri: &SymbolURI) -> Self {
223        let module = uri.module();
224        let archive = module.archive_id().clone();
225        let path = if let Some(p) = module.path() {
226            format!("{p}?{}", module.name().first_name())
227        } else {
228            module.name().first_name().to_string()
229        };
230        Self {
231            kind: "usemodule",
232            archive,
233            path,
234        }
235    }
236}
237
238#[derive(leptos::server_fn::serde::Serialize, Debug, Clone)]
239struct Preview<'u> {
240    kind: &'static str,
241    uri: &'u SymbolURI,
242}
243impl Preview<'_> {
244    fn make(uri: &SymbolURI) -> Preview<'_> {
245        Preview {
246            kind: "preview",
247            uri,
248        }
249    }
250}
251
252#[derive(Copy, Clone)]
253struct Short<'u>(&'u SymbolURI);
254impl std::fmt::Display for Short<'_> {
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        write!(f, "[{}]{{", self.0.archive_id())?;
257        if let Some(p) = self.0.path() {
258            p.fmt(f)?;
259            f.write_char('?')?;
260        }
261        write!(f, "{}}} {}", self.0.module().name(), self.0.name())
262    }
263}
264
265fn do_sym_result_local(sym: &SymbolURI) -> impl IntoView + use<> {
266    let vs = unwrap!(VSCode::get());
267    let name = ftml_viewer_components::components::omdoc::symbol_name(sym, &Short(sym).to_string());
268    view! {
269        <div class="flams-search-block">
270            <div><b>{name}</b>
271                {
272                    let sym_a = sym.clone();
273                    let vs_a = vs.clone();
274                    let on_use = move |_| {
275                        let _ = vs_a.post_message(Usemodule::make(&sym_a));
276                    };
277                    let sym = sym.clone();
278                    let on_preview = move |_| {
279                        let _ = vs.post_message(Preview::make(&sym));
280                    };
281                    view!{
282                        <div style="width:100%">
283                            <div style="margin-left:auto;width:fit-content;display:flex;flex-direction:row;">
284                                <div style="width:fit-content;margin-right:5px;" on:click=on_preview>
285                                    <VSCodeButton>"preview"</VSCodeButton>
286                                </div>
287                                <div style="width:fit-content;" on:click=on_use>
288                                    <VSCodeButton>"\\usemodule"</VSCodeButton>
289                                </div>
290                            </div>
291                        </div>
292                    }
293                }
294            </div>
295        </div>
296    }
297}
298
299fn do_sym_result_remote(
300    sym: &SymbolURI,
301    res: Vec<(f32, SearchResult)>,
302    remote: fn() -> Option<String>,
303) -> impl IntoView + use<> {
304    use thaw::Scrollbar;
305    let name = ftml_viewer_components::components::omdoc::symbol_name(sym, &sym.to_string());
306    view! {
307        <div class="flams-search-block">
308            <div><b>{name}</b>
309            </div>
310            <div style="display:block">
311            <div style="padding:0 5px;max-width:100%">
312                <div style="width:100%;color:black;background-color:white;">
313                  <Scrollbar style="max-height: 100px;width:100%;max-width:100%;">{
314                    res.into_iter().map(|(_,r)| {
315                      let SearchResult::Paragraph { uri, .. } = r else { impossible!()};
316                      fragment(uri.into(),Some(remote))
317                    }).collect_view()
318                  }
319                  </Scrollbar>
320                </div>
321              </div>
322            </div>
323        </div>
324    }
325}
326
327fn do_doc(score: f32, uri: DocumentURI, remote: Option<fn() -> Option<String>>) -> impl IntoView {
328    use thaw::Scrollbar;
329    let name = doc_name(&uri, uri.name().to_string());
330    view! {
331        <div class="flams-search-block">
332            <div><b>"Document "{name}</b>
333                <div style="width:100%"><div style="margin-left:auto;width:fit-content;">"Score: "{score}</div></div>
334            </div>
335            <div style="display:block">
336            <div style="padding:0 5px;max-width:100%">
337                <div style="width:100%;color:black;background-color:white;">
338                  <Scrollbar style="max-height: 100px;width:100%;max-width:100%;">
339                    {fragment(uri.into(),remote)}
340                  </Scrollbar>
341                </div>
342              </div>
343            </div>
344        </div>
345    }
346}
347
348fn do_para(
349    score: f32,
350    uri: DocumentElementURI,
351    kind: SearchResultKind,
352    fors: Vec<SymbolURI>,
353    remote: Option<fn() -> Option<String>>,
354) -> impl IntoView {
355    use thaw::Scrollbar;
356    let uristr = uri.to_string();
357    let name = uristr;
358    let desc = comma_sep(
359        "For",
360        fors.into_iter()
361            .map(|s| symbol_name(&s, s.name().last_name().as_ref())),
362    );
363    view! {
364        <div class="flams-search-block">
365            <div><b>{kind.as_str()}" "{name}</b>
366                <div style="width:100%"><div style="margin-left:auto;width:fit-content;">"Score: "{score}</div></div>
367            </div>
368            <div style="display:block">
369            <div style="padding:0 5px;max-width:100%">
370                <div style="width:100%;color:black;background-color:white;">
371                  <Scrollbar style="max-height: 100px;width:100%;max-width:100%;">
372                    {fragment(uri.into(),remote)}
373                  </Scrollbar>
374                </div>
375              </div>
376            </div>
377        </div>
378    }
379}
380
381fn fragment(uri: NarrativeURI, remote: Option<fn() -> Option<String>>) -> impl IntoView {
382    use flams_router_content::components::Fragment;
383    use leptos::either::Either;
384    move || {
385        let uri = uri.clone();
386        if let Some(remote) = remote.and_then(|f| f()) {
387            Either::Left({
388                #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
389                {
390                    use flams_router_base::ServerFnExt;
391                    use ftml_viewer_components::components::documents::{
392                        FragmentString, FragmentStringProps,
393                    };
394                    wait_and_then_fn(
395                        move || {
396                            flams_router_content::server_fns::Fragment {
397                                uri: Some(URI::Narrative(uri.clone())),
398                                rp: None,
399                                a: None,
400                                p: None,
401                                l: None,
402                                d: None,
403                                e: None,
404                                s: None,
405                                m: None,
406                                context: None,
407                            }
408                            .call_remote(remote.clone())
409                        },
410                        move |(uri, css, html)| {
411                            let uri = if let URI::Narrative(NarrativeURI::Element(uri)) = uri {
412                                Some(uri)
413                            } else {
414                                None
415                            };
416                            view! {<div>{
417                              for css in css { do_css(css); }
418                              FragmentString(FragmentStringProps{html,uri})
419                            }</div>}
420                        },
421                    )
422                }
423                #[cfg(not(feature = "hydrate"))]
424                {
425                    ""
426                }
427            })
428        } else {
429            Either::Right(view!(<Fragment uri=URIComponents::Uri(URI::Narrative(uri)) />))
430        }
431    }
432}