1use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
2
3use bevy_ecs::{
4 prelude::*, schedule::ScheduleLabel, system::RunSystemOnce as _, world::CommandQueue,
5};
6use futures::{channel, SinkExt};
7use rdf_parsers::{
8 jsonld::{
9 convert::{
10 convert_with_loader, parse_jsonld_for_context, ActiveContext, ContextLoader, JsonLdVal,
11 },
12 parser::{Lang, Rule, SyntaxKind},
13 },
14 IncrementalBias, PrevParseInfo,
15};
16use rowan::{GreenNode, NodeOrToken};
17use swls_core::{
18 lsp_types::{request::InlayHintRefreshRequest, TextDocumentItem, Url},
19 prelude::*,
20};
21use swls_lang_turtle::{ecs::parse::derive_triples_system, lang::parser::TurtleParseError};
22use tracing::instrument;
23
24use crate::{cjs, JsonLdLang, Registry};
25
26pub mod completion;
27pub use completion::setup_completion;
28
29#[derive(Resource, Default)]
31pub struct ContextCache(pub HashMap<String, String>);
32
33#[derive(Component, Debug, Default, Clone)]
38pub struct JsonLdActiveContext(pub ActiveContext);
39
40struct WorldContextLoader {
51 sender: CommandSender,
52 cjs_contexts: Arc<HashMap<String, JsonLdVal>>,
54 local_cache: HashMap<String, JsonLdVal>,
56}
57
58impl ContextLoader for WorldContextLoader {
59 fn load_val<'a>(
60 &'a mut self,
61 url: &'a str,
62 ) -> Pin<Box<dyn Future<Output = Option<rdf_parsers::jsonld::convert::JsonLdVal>> + 'a>> {
63 if let Some(val) = self.local_cache.get(url) {
65 let val = val.clone();
66 return Box::pin(std::future::ready(Some(val)));
67 }
68
69 if let Some(result) = self.cjs_contexts.get(url).map(|ctx_doc| {
71 if let Some(ctx) = ctx_doc.get("@context") {
72 ctx.clone()
73 } else {
74 ctx_doc.clone()
75 }
76 }) {
77 self.local_cache.insert(url.to_string(), result.clone());
78 return Box::pin(std::future::ready(Some(result)));
79 }
80
81 tracing::debug!("context loader load val {}", url);
83 let cs = self.sender.clone();
84 let url = url.to_string();
85 Box::pin(async move {
86 let (sender, receiver) = futures::channel::oneshot::channel::<Result<String, _>>();
87 let mut command_queue = CommandQueue::default();
88 let url2 = url.clone();
89 tracing::debug!("Trying to find context {}", url.as_str());
90 command_queue.push(move |world: &mut World| {
91 world.spawn(ContextRequest {
92 url: url2,
93 on_ready: Some(sender),
94 });
95 world.run_schedule(FetchLabel);
96 });
97
98 let _ = cs.unbounded_send(command_queue);
99
100 let v = receiver.await.ok()?;
101 tracing::debug!("context loader got response for {}", url);
102 match v {
103 Ok(s) => parse_jsonld_for_context(&s),
104 Err(x) => {
105 if let JsonLdVal::Object(members, _) = &x {
106 if let Some((_, _, _, ctx)) =
107 members.iter().find(|(k, _, _, _)| k == "@context")
108 {
109 return Some(ctx.clone());
110 }
111 }
112
113 Some(x)
114 }
115 }
116 })
117 }
118 fn load<'a>(&'a mut self, url: &'a str) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>> {
119 tracing::debug!("context loader load string {}", url);
120 Box::pin(std::future::ready(None))
121 }
122}
123
124pub fn setup_parsing<C: Client + Resource + Clone>(world: &mut World) {
125 use swls_core::feature::parse::*;
126 world.schedule_scope(ParseLabel, |_, schedule| {
127 schedule.add_systems((
128 parse_jsonld_system::<C>,
129 derive_jsonld_prefixes
130 .after(parse_jsonld_system::<C>)
131 .before(prefixes),
132 derive_triples_system::<JsonLdLang>
133 .after(parse_jsonld_system::<C>)
134 .before(triples),
135 ));
136 });
137
138 let mut schedule = bevy_ecs::schedule::Schedule::new(FetchLabel);
139 schedule.add_systems((
140 cjs::cjs_loader.before(fetch_from_world),
141 fetch_from_world,
142 fetch_from_web::<C>.after(fetch_from_world),
143 return_empty.after(fetch_from_web::<C>),
144 ));
145 world.add_schedule(schedule);
146}
147
148#[derive(ScheduleLabel, Clone, Eq, PartialEq, Debug, Hash)]
150pub struct FetchLabel;
151
152fn return_empty(requests: Query<(Entity, &mut ContextRequest)>, mut commander: Commands) {
153 for (e, mut r) in requests {
154 if let Some(sender) = r.on_ready.take() {
155 tracing::debug!("loader sending empty {}", r.url);
156 let _ = sender.send(Result::Ok(String::new()));
157 } else {
158 tracing::debug!("loader cannot send empty {}", r.url);
159 }
160 commander.entity(e).despawn();
161 }
162}
163
164fn fetch_from_world(
165 mut requests: Query<(Entity, &mut ContextRequest)>,
166 documents: Query<(&Label, Option<&Wrapped<JsonLdVal>>, Option<&Source>)>,
167 mut commander: Commands,
168) {
169 for (entity, mut request) in requests.iter_mut() {
183 for (labels, jsonld_val, s) in documents {
184 if labels.as_str() == request.url.as_str() {
185 if let Some(sender) = request.on_ready.take() {
186 let o = match (jsonld_val, s) {
187 (Some(val), _) => {
188 let char_count = format!("{:?}", val).len();
189 tracing::debug!(
190 "loader fetch_from_world sent {} (found val {} {})",
191 request.url,
192 jsonld_val.is_some(),
193 char_count
194 );
195 sender.send(Result::Err(val.as_ref().clone()))
196 }
197 (_, Some(source)) => {
198 tracing::debug!(
199 "loader fetch_from_world sent {} (found val false {})",
200 request.url,
201 source.len()
202 );
203 sender.send(Result::Ok(source.to_string()))
204 }
205 _ => {
206 tracing::debug!(
207 "loader fetch_from_world sent {} (empty string)",
208 request.url,
209 );
210 sender.send(Result::Ok("".to_string()))
211 }
212 };
213 tracing::debug!("Sent successful {:?}", o);
214 }
215 if let Ok(mut e) = commander.get_entity(entity) {
216 e.despawn();
217 }
218 }
219 }
220 }
221}
222
223fn fetch_from_web<C: Client + Resource + Clone>(
224 mut requests: Query<(&mut ContextRequest,), Added<ContextRequest>>,
225 sender: Res<CommandSender>,
226 client: Res<C>,
227 fs: Res<Fs>,
228) {
229 for (r,) in requests.iter_mut() {
230 let client_clone = client.as_ref().clone();
231 let url = r.url.to_string();
232
233 let Ok(label) = Url::parse(&url) else {
234 continue;
235 };
236 let sender = sender.clone();
237 if label.scheme().starts_with("http") {
238 client.spawn(async move {
239 if let Ok(resp) = client_clone.fetch(&url, &HashMap::new()).await {
240 let mut cq = CommandQueue::default();
241 cq.push(move |world: &mut World| {
242 if world
243 .query::<(Entity, &Label)>()
244 .iter(&world)
245 .find(|x| x.1.as_str() == &url)
246 .is_none()
247 {
248 world.spawn((Source(resp.body), Label(label)));
249 let _ = world.run_system_once(derive_jsonld_triples::<C>);
250 }
251 });
252 let _ = sender.unbounded_send(cq);
253 }
254 });
255 } else {
256 let fs = fs.clone();
257 client.spawn(async move {
258 if let Some(resp) = fs.0.read_file(&label).await {
259 let mut cq = CommandQueue::default();
260 cq.push(move |world: &mut World| {
261 if world
262 .query::<(Entity, &Label)>()
263 .iter(&world)
264 .find(|x| x.1.as_str() == &url)
265 .is_none()
266 {
267 let item = TextDocumentItem {
268 version: 1,
269 uri: label.clone(),
270 language_id: String::from("turtle"),
271 text: String::new(),
272 };
273 let e = world
274 .spawn((
275 RopeC(LineIndex::new(&resp)),
276 Source(resp),
277 Label(label.clone()),
278 Wrapped(item),
279 Types(HashMap::new()),
280 ))
281 .id();
282
283 world.trigger(CreateEvent {
284 url: label,
285 language_id: Some("jsonld".to_string()),
286 entity: e,
287 });
288 tracing::debug!("Found local file, rerunning the deriving {}", url);
289 let _ = world.run_system_once(derive_jsonld_triples::<C>);
290 }
291 });
292 let _ = sender.unbounded_send(cq);
293 }
294 });
295 }
296 }
297}
298
299#[derive(bevy_ecs::prelude::Component)]
300pub struct ContextRequest {
301 pub url: String,
302 pub on_ready: Option<channel::oneshot::Sender<Result<String, JsonLdVal>>>,
303}
304
305fn extract_jsonld_cst_tokens(node: &rowan::SyntaxNode<Lang>) -> Vec<Spanned<rowan::SyntaxKind>> {
306 let mut tokens = Vec::new();
307 for not in node.descendants_with_tokens() {
308 if let NodeOrToken::Token(t) = not {
309 if t.kind() == SyntaxKind::WhiteSpace {
310 continue;
311 }
312 let range = t.text_range();
313 let span = usize::from(range.start())..usize::from(range.end());
314 tokens.push(spanned(rowan::SyntaxKind(t.kind() as u16), span));
315 }
316 }
317 tokens
318}
319
320fn collect_errors(node: &rowan::SyntaxNode<Lang>) -> Vec<TurtleParseError> {
321 let mut errors = Vec::new();
322 let mut stack = vec![node.clone()];
323 while let Some(current) = stack.pop() {
324 for child in current.children_with_tokens() {
325 match child {
326 NodeOrToken::Node(n) => {
327 if n.kind() == SyntaxKind::Error {
328 let range = rdf_parsers::effective_error_span::<Lang>(&n);
329 let msg = n
330 .parent()
331 .map(|p| format!("Expected: {:?}", p.kind()))
332 .unwrap_or_else(|| format!("Unexpected: {}", n.text()));
333 errors.push(TurtleParseError { range, msg });
334 } else {
335 stack.push(n);
336 }
337 }
338 NodeOrToken::Token(t) => {
339 if t.kind() == SyntaxKind::Error {
340 let r = t.text_range();
341 errors.push(TurtleParseError {
342 range: r.start().into()..r.end().into(),
343 msg: format!("Unexpected: {}", t.text()),
344 });
345 }
346 }
347 }
348 }
349 }
350 errors
351}
352
353pub fn derive_jsonld_triples<C: Client + Resource + Clone>(
362 query: Query<(Entity, &Wrapped<GreenNode>, &Label, &Source), With<JsonLdLang>>,
363 sender: Res<CommandSender>,
364 client: Res<C>,
365 registry: Res<Registry>,
366) {
367 let items: Vec<(Entity, rowan::GreenNode, String, usize)> = query
369 .iter()
370 .map(|(e, gn, label, source)| (e, gn.0.clone(), label.0.to_string(), source.0.len()))
371 .collect();
372
373 if items.is_empty() {
374 return;
375 }
376
377 let cjs_contexts = registry.1.contexts.clone();
378 let mut sender_clone = sender.clone();
379 let c2 = client.clone();
380
381 client.spawn_local(move || async move {
382 let mut all_results: Vec<(Entity, Element, JsonLdActiveContext)> =
383 Vec::with_capacity(items.len());
384
385 for (e, gn, base, span_len) in items {
386 let mut loader = WorldContextLoader {
387 sender: sender_clone.clone(),
388 cjs_contexts: cjs_contexts.clone(),
389 local_cache: HashMap::new(),
390 };
391 let syntax = rowan::SyntaxNode::new_root(gn);
392 let (jsonld_model, ctx) = convert_with_loader(&syntax, &mut loader, Some(base)).await;
393
394 tracing::debug!("{} triples", jsonld_model.triples.len());
395 let element = Element(spanned(jsonld_model, 0..span_len));
396 all_results.push((e, element, JsonLdActiveContext(ctx)));
397 }
398
399 let mut command_queue = CommandQueue::default();
402 command_queue.push(move |world: &mut World| {
403 for (e, element, ctx) in all_results {
404 world.entity_mut(e).insert((element, ctx));
405 }
406 world.run_schedule(ParseLabel);
407 });
408 let _ = sender_clone.0.send(command_queue).await;
409 c2.send_request::<InlayHintRefreshRequest>(()).await;
410 });
411}
412
413#[instrument(skip(query, sender, commands, client, registry))]
414fn parse_jsonld_system<C: Client + Resource + Clone>(
415 query: Query<
416 (Entity, &Source, &Label, Option<&Wrapped<PrevParseInfo>>),
417 (Changed<Source>, With<JsonLdLang>, With<Open>),
418 >,
419 mut commands: Commands,
420 sender: Res<CommandSender>,
421 client: Res<C>,
422 registry: Res<Registry>,
423 config: Res<ServerConfig>,
424) {
425 if !config.config.jsonld.unwrap_or(true) {
426 return;
427 }
428 let cjs_contexts = registry.1.contexts.clone();
429
430 for (entity, source, label, prev) in &query {
431 let (parse, new_prev) = rdf_parsers::parse_incremental(
432 Rule::new(SyntaxKind::JsonldDoc),
433 source.0.as_str(),
434 prev.map(|x| &x.0),
435 IncrementalBias::default(),
436 );
437 commands.entity(entity).insert(Wrapped(new_prev));
438
439 let syntax = parse.syntax::<Lang>();
440 let errors = collect_errors(&syntax);
441 let cst_tokens = extract_jsonld_cst_tokens(&syntax);
442
443 let gn = parse.green_node.clone();
444 let base = label.0.to_string();
445 let span = 0..source.0.len();
446 let e = entity.clone();
447 let mut sender = sender.clone();
448 let cjs_contexts = cjs_contexts.clone();
449 client.spawn_local(move || async move {
450 let mut loader = WorldContextLoader {
451 sender: sender.clone(),
452 cjs_contexts,
453 local_cache: HashMap::new(),
454 };
455 let syntax = rowan::SyntaxNode::new_root(gn.clone());
456 let (jsonld_model, ctx) = convert_with_loader(&syntax, &mut loader, Some(base)).await;
457
458 tracing::debug!("{} triples", jsonld_model.triples.len());
459
460 let element = Element(spanned(jsonld_model, span));
461
462 let mut command_queue = CommandQueue::default();
463 command_queue.push(move |world: &mut World| {
464 world
465 .entity_mut(e)
466 .insert((element, JsonLdActiveContext(ctx)));
467 world.run_schedule(ParseLabel);
473 });
474 let _ = sender.0.send(command_queue).await;
475 });
476
477 if errors.is_empty() {
478 commands
479 .entity(entity)
480 .insert((
481 Errors(errors),
482 CstTokens(cst_tokens),
483 Wrapped(parse.green_node),
484 ))
485 .remove::<Dirty>();
486 } else {
487 commands.entity(entity).insert((
488 Errors(errors),
489 CstTokens(cst_tokens),
490 Wrapped(parse.green_node),
491 Dirty,
492 ));
493 }
494 }
495}
496
497fn derive_jsonld_prefixes(
504 query: Query<(Entity, &Label, &Element), (Changed<Element>, With<JsonLdLang>)>,
505 mut commands: Commands,
506) {
507 use swls_lang_rdf_base::traits::NamedNodeExt;
508
509 for (entity, url, turtle) in &query {
510 let prefixes: Vec<_> = turtle
511 .prefixes
512 .iter()
513 .flat_map(|prefix| {
514 let expanded = prefix.value.value().expand(turtle.value())?;
515 let parsed_url = swls_core::lsp_types::Url::parse(&expanded).ok()?;
516 Some(Prefix {
517 url: parsed_url,
518 prefix: prefix.prefix.value().clone(),
519 })
520 })
521 .collect();
522
523 let base = turtle
524 .base
525 .as_ref()
526 .and_then(|b| {
527 b.0 .1
528 .value()
529 .expand(turtle.value())
530 .and_then(|x| swls_core::lsp_types::Url::parse(&x).ok())
531 })
532 .unwrap_or_else(|| url.0.clone());
533
534 commands.entity(entity).insert(Prefixes(prefixes, base));
535 }
536}
537
538pub(crate) fn format_jsonld_system(
539 mut query: Query<(&RopeC, &Wrapped<GreenNode>, &mut FormatRequest), With<JsonLdLang>>,
540 config: Res<ServerConfig>,
541) {
542 use swls_core::lsp_types::{Position, Range};
543 if !config.config.format.jsonld() {
544 tracing::debug!("JSON-LD formatting disabled by config");
545 return;
546 }
547 for (source, node, mut request) in &mut query {
548 if request.0.is_some() {
549 tracing::debug!("Didn't format with the jsonld format system, already formatted");
550 continue;
551 }
552 tracing::debug!("Formatting with turtle format system");
553
554 let root = rowan::SyntaxNode::new_root(node.0.clone());
555
556 let formatted = rdf_parsers::jsonld::format::format(&root, 80);
557
558 request.0 = Some(vec![swls_core::lsp_types::TextEdit::new(
559 Range::new(
560 Position::new(0, 0),
561 Position::new(source.0.len_lines() as u32 + 1, 0),
562 ),
563 formatted,
564 )]);
565 }
566}
567
568#[cfg(test)]
569mod id_highlight {
570 use swls_core::feature::semantic::{HighlightRequest, TokenTypesComponent};
571 use swls_core::feature::{ParseLabel, SemanticLabel};
572 use swls_core::lsp_types::SemanticTokenType;
573 use swls_core::prelude::*;
574 use swls_test_utils::{create_file, setup_world, TestClient};
575
576 #[test]
580 fn id_iris_consistently_highlighted_not_string() {
581 let (mut world, _) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
582 let src = "{\n \"@context\": { \"ex\": \"https://example.org/\", \"sosa\": \"http://www.w3.org/ns/sosa/\" },\n \"@graph\": [\n { \"@type\": \"sosa:Message\", \"sosa:primaryTopic\": { \"@id\": \"ex:obs1\" } },\n { \"@id\": \"ex:obs1\", \"sosa:isObservedBy\": { \"@id\": \"ex:sensor-1\" } }\n ]\n}";
583 let entity = create_file(&mut world, src, "http://example.com/ns#", "jsonld", Open);
584 world.run_schedule(ParseLabel);
585 let client = world.resource::<TestClient>().clone();
586 futures::executor::block_on(client.await_futures(|| world.run_schedule(swls_core::Tasks)));
587 world.entity_mut(entity).insert(HighlightRequest(vec![]));
588 world.run_schedule(SemanticLabel);
589
590 let ttc = world.entity(entity).get::<TokenTypesComponent>().expect("ttc");
591 let mut ts: Vec<Option<SemanticTokenType>> = vec![None; src.len()];
592 for s in &ttc.0 {
593 for j in s.span().clone() {
594 if j < ts.len() {
595 ts[j] = Some(s.value().clone());
596 }
597 }
598 }
599 let ty_at = |b: usize| ts[b].as_ref().map(|t| t.as_str().to_string());
600
601 let check = |iri: &str| {
606 let quoted = format!("\"{iri}\"");
607 let colon = iri.find(':').unwrap();
608 for (q, _) in src.match_indices("ed) {
609 let open = q; let content = q + 1; let close = q + 1 + iri.len(); assert_ne!(
613 ty_at(open).as_deref(),
614 Some("namespace"),
615 "opening quote of {quoted} at {open} must not be coloured as the IRI"
616 );
617 for b in content..content + colon + 1 {
619 assert_eq!(
620 ty_at(b).as_deref(),
621 Some("namespace"),
622 "{quoted}: byte {b} ({:?}) should be NAMESPACE",
623 &src[b..b + 1]
624 );
625 }
626 for b in content + colon + 1..close {
628 assert_eq!(
629 ty_at(b).as_deref(),
630 Some("enumMember"),
631 "{quoted}: byte {b} ({:?}) should be enumMember (not string)",
632 &src[b..b + 1]
633 );
634 }
635 }
636 };
637 check("ex:obs1");
638 check("ex:sensor-1");
639 }
640
641 #[test]
647 fn context_keywords_and_literals_coloured_correctly() {
648 let (mut world, _) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
649 let src = "{\n \"@context\": { \"ex\": \"https://example.org/\" },\n \"@id\": \"ex:alice\",\n \"@type\": \"ex:Person\",\n \"ex:label\": \"Alice\"\n}";
650 let entity = create_file(&mut world, src, "http://example.com/ns#", "jsonld", Open);
651 world.run_schedule(ParseLabel);
652 let client = world.resource::<TestClient>().clone();
653 futures::executor::block_on(client.await_futures(|| world.run_schedule(swls_core::Tasks)));
654 world.entity_mut(entity).insert(HighlightRequest(vec![]));
655 world.run_schedule(SemanticLabel);
656
657 let ttc = world.entity(entity).get::<TokenTypesComponent>().expect("ttc");
658 let mut ts: Vec<Option<SemanticTokenType>> = vec![None; src.len()];
659 for s in &ttc.0 {
660 for j in s.span().clone() {
661 if j < ts.len() {
662 ts[j] = Some(s.value().clone());
663 }
664 }
665 }
666 let all = |needle: &str, expected: &str| {
668 let q = src.find(needle).unwrap_or_else(|| panic!("missing {needle:?}"));
669 for b in q..q + needle.len() {
670 assert_eq!(
671 ts[b].as_ref().map(|t| t.as_str()),
672 Some(expected),
673 "{needle:?} byte {b} ({:?})",
674 &src[b..b + 1]
675 );
676 }
677 };
678
679 all("\"ex\"", "namespace"); all("\"@id\"", "keyword"); all("\"@type\"", "keyword"); all("\"Alice\"", "string"); all("\"https://example.org/\"", "string"); }
685}