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}