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 {do_results("Local Results",None,local_results)}
158 <div style="margin-top:25px;"></div>
159 {do_results("Remote Results",Some(remote),remote_results)}
160 </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 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>(); 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>(); 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>(); 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 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 }</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}