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 let sig = RwSignal::new(false);
28 Effect::new(move || {
29 #[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 {do_results("Local Results",None,local_results)}
187 <div style="margin-top:25px;"></div>
188 {do_results("Remote Results",Some(remote),remote_results)}
189 </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 ); 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(); 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(); 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 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 }</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}