flams_router_search/
vscode.rs

1use std::fmt::Write;
2
3use crate::components::SearchState;
4use flams_backend_types::search::{QueryFilter, SearchResult, SearchResultKind};
5use flams_router_vscode::{
6    VSCode,
7    components::{VSCodeButton, VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup, VSCodeTextbox},
8};
9use flams_utils::{impossible, unwrap};
10use flams_web_utils::components::wait_and_then_fn;
11use ftml_components::components::content::FtmlViewable;
12use ftml_dom::{FtmlViews, utils::css::inject_css};
13use ftml_uris::{
14    ArchiveId, DocumentElementUri, DocumentUri, IsDomainUri, IsNarrativeUri, NarrativeUri,
15    SymbolUri, UriWithArchive, UriWithPath,
16    components::{UriComponents, UriComponentsTrait},
17};
18use leptos::prelude::*;
19
20#[component]
21pub fn VSCodeSearch() -> AnyView {
22    flams_router_content::Views::top(move || {
23        let remote = || leptos_router::hooks::use_query_map().with(|q| q.get("remote"));
24
25        let selected_radio = RwSignal::new(Some("doc".to_string()));
26        let disabled =
27            Memo::new(move |_| selected_radio.with(|s| s.as_ref().is_some_and(|s| s == "symbol")));
28
29        let full_docs = RwSignal::new(false);
30        let paras = RwSignal::new(true);
31        let defs = RwSignal::new(true);
32        let exs = RwSignal::new(true);
33        let asss = RwSignal::new(false);
34        let probs = RwSignal::new(false);
35        let query = RwSignal::new(String::default());
36        let opts = Memo::new(move |_| {
37            let mut ret = QueryFilter::default();
38            ret.allow_documents = full_docs.get();
39            ret.allow_paragraphs = paras.get();
40            ret.allow_definitions = defs.get();
41            ret.allow_examples = exs.get();
42            ret.allow_assertions = asss.get();
43            ret.allow_problems = probs.get();
44            ret
45        });
46        let local_results = RwSignal::new(SearchState::None);
47        let remote_results = RwSignal::new(SearchState::None);
48        let local_act = Action::new(move |&()| {
49            let query = query.get_untracked();
50            local_results.set(SearchState::Loading);
51            let opts = opts.get_untracked();
52            async move {
53                match super::search_query(query, opts, 20).await {
54                    Ok(r) => local_results.set(SearchState::Results(r)),
55                    Err(_) => {
56                        local_results.set(SearchState::None);
57                    }
58                }
59            }
60        });
61        let remote_act = Action::new(move |&()| {
62            let remote = remote();
63            let query = query.get_untracked();
64            remote_results.set(SearchState::Loading);
65            let opts = opts.get_untracked();
66            async move {
67                let Some(remote) = remote else { return };
68                #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
69                {
70                    use flams_router_base::ServerFnExt;
71                    let query = super::SearchQuery {
72                        query,
73                        opts,
74                        num_results: 20,
75                    }
76                    .call_remote(remote)
77                    .await;
78                    match query {
79                        Ok(r) => remote_results.set(SearchState::Results(r)),
80                        Err(_) => {
81                            remote_results.set(SearchState::None);
82                        }
83                    }
84                }
85            }
86        });
87        let local_sym_act = Action::new(move |&()| {
88            let query = query.get_untracked();
89            local_results.set(SearchState::Loading);
90            async move {
91                match super::search_symbols(query, 20).await {
92                    Ok(r) => local_results.set(SearchState::SymResults(r)),
93                    Err(_) => {
94                        local_results.set(SearchState::None);
95                    }
96                }
97            }
98        });
99        let remote_sym_act = Action::new(move |&()| {
100            let remote = remote();
101            let query = query.get_untracked();
102            remote_results.set(SearchState::Loading);
103            async move {
104                let Some(remote) = remote else { return };
105                #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
106                {
107                    use flams_router_base::ServerFnExt;
108                    let query = super::SearchSymbols {
109                        query,
110                        num_results: 20,
111                    }
112                    .call_remote(remote)
113                    .await;
114                    match query {
115                        Ok(r) => remote_results.set(SearchState::SymResults(r)),
116                        Err(_) => {
117                            remote_results.set(SearchState::None);
118                        }
119                    };
120                }
121            }
122        });
123        Effect::new(move || {
124            if query.with(String::is_empty) {
125                local_results.set(SearchState::None);
126                return;
127            }
128            if selected_radio.with(|v| v.as_ref().is_some_and(|s| s == "symbol")) {
129                local_sym_act.dispatch(());
130                remote_sym_act.dispatch(());
131            } else {
132                let _ = opts.get();
133                local_act.dispatch(());
134                remote_act.dispatch(());
135            }
136        });
137
138        inject_css("flams-search-block", include_str!("vscode.css"));
139        view! {
140            <div style="display:flex;flex-direction:column;">
141                <VSCodeTextbox value=query placeholder="Search"/>
142                <VSCodeRadioGroup name="flams-vscode-search" selected=selected_radio>
143                    <div style="display:flex;flex-direction:row;">
144                        <VSCodeRadio id="symbol">"Symbols"</VSCodeRadio>
145                        <VSCodeRadio id="doc">"Paragraphs"</VSCodeRadio>
146                    </div>
147                </VSCodeRadioGroup>
148                <div style="display:flex;flex-direction:row;flex-wrap:wrap;">
149                    <VSCodeCheckbox checked=full_docs disabled>"Full Documents"</VSCodeCheckbox>
150                    <VSCodeCheckbox checked=paras disabled>"Paragraphs"</VSCodeCheckbox>
151                    <VSCodeCheckbox checked=defs disabled>"Definitions"</VSCodeCheckbox>
152                    <VSCodeCheckbox checked=exs disabled>"Examples"</VSCodeCheckbox>
153                    <VSCodeCheckbox checked=asss disabled>"Assertions"</VSCodeCheckbox>
154                    <VSCodeCheckbox checked=probs disabled>"Problems"</VSCodeCheckbox>
155                    /*<Themer>*///{
156                        //flams_router_content::Views::top(move || view!{
157                            {do_results("Local Results",None,local_results)}
158                            <div style="margin-top:25px;"></div>
159                            {do_results("Remote Results",Some(remote),remote_results)}
160                        // })
161                    //}//</Themer>
162                </div>
163            </div>
164        }
165    }.into_any()).into_any()
166}
167
168fn do_results(
169    pre: &'static str,
170    remote: Option<fn() -> Option<String>>,
171    results: RwSignal<SearchState>,
172) -> AnyView {
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    }).into_any()
199}
200
201fn do_result(
202    score: f32,
203    res: &SearchResult,
204    remote: Option<fn() -> Option<String>>,
205) -> AnyView {
206    use leptos::either::Either::*;
207    match res {
208        SearchResult::Document(d) => do_doc(score, d.clone(), remote),
209        SearchResult::Paragraph {
210            uri, fors, kind, ..
211        } => 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_uri();
224        let archive = module.archive_id().clone();
225        let path = if let Some(p) = module.path() {
226            format!("{p}?{}", module.module_name().first())
227        } else {
228            module.module_name().first().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) -> AnyView {
266    let vs = unwrap!(VSCode::get());
267    let name = sym.as_view::<flams_router_content::backend::FtmlBackend>(); //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    }.into_any()
297}
298
299fn do_sym_result_remote(
300    sym: &SymbolUri,
301    res: Vec<(f32, SearchResult)>,
302    remote: fn() -> Option<String>,
303) -> AnyView {
304    use thaw::Scrollbar;
305    let name = sym.as_view::<flams_router_content::backend::FtmlBackend>(); //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    }.into_any()
325}
326
327fn do_doc(score: f32, uri: DocumentUri, remote: Option<fn() -> Option<String>>) -> AnyView {
328    use thaw::Scrollbar;
329    let name = uri.as_view::<flams_router_content::backend::FtmlBackend>(); //doc_name(&uri, uri.document_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    }.into_any()
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) -> AnyView {
355    use thaw::Scrollbar;
356    let uristr = uri.to_string();
357    let name = uristr;
358    /*let desc = ftml_components::components::content::CommaSep(
359        "For",
360        fors.into_iter()
361            .map(|s| s.as_view::<flams_router_content::backend::FtmlBackend>()),
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    }.into_any()
379}
380
381fn fragment(uri: NarrativeUri, remote: Option<fn() -> Option<String>>) -> AnyView {
382    use flams_router_content::components::Fragment;
383    (move || {
384        let uri = uri.clone();
385        if let Some(remote) = remote.and_then(|f| f()) {
386            {
387                #[cfg(all(feature = "hydrate", not(feature = "ssr")))]
388                {
389                    use flams_router_base::ServerFnExt;
390                    wait_and_then_fn(
391                        move || {
392                            flams_router_content::server_fns::Fragment {
393                                uri: Some(uri.clone().into()),
394                                rp: None,
395                                a: None,
396                                p: None,
397                                l: None,
398                                d: None,
399                                e: None,
400                                s: None,
401                                m: None,
402                                context: None,
403                            }
404                            .call_remote(remote.clone())
405                        },
406                        move |(uri, css, html)| {
407                            use ftml_dom::utils::css::CssExt;
408                            use ftml_uris::Uri;
409
410                            let uri = if let Uri::DocumentElement(uri) = uri {
411                                Some(uri)
412                            } else {
413                                None
414                            };
415                            view! {<div>{
416                              for css in css { css.inject(); }
417                              flams_router_content::Views::render_ftml(html.into_string(),None)
418                              //FragmentString(FragmentStringProps{html,uri})
419                            }</div>}.into_any()
420                        },
421                    )
422                }
423                #[cfg(not(feature = "hydrate"))]
424                {
425                    ""
426                }
427            }.into_any()
428        } else {
429            view!(<Fragment uri=UriComponents::Full(uri.into()) position=ftml_components::SidebarPosition::None/>).into_any()
430        }
431    }).into_any()
432}