Skip to main content

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