1#![doc(
2 html_logo_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.png",
3 html_favicon_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.ico"
4)]
5pub mod cjs;
6
7use std::{borrow::Cow, ops::Range};
8
9use bevy_ecs::{
10 component::Component,
11 observer::On,
12 query::With,
13 resource::Resource,
14 schedule::IntoScheduleConfigs,
15 system::{Commands, Query, Res, RunSystemOnce},
16 world::{CommandQueue, World},
17};
18use components_rs::{
19 components::registry::{resolve_iri_to_url, ComponentRegistry},
20 module_state::ModuleState,
21};
22use oxigraph::model::{GraphName, Literal, NamedNode, Quad};
23use swls_core::{
24 lang::{Lang, LangHelper},
25 lsp_types::{SemanticTokenType, Url},
26 prelude::{goto_definition::GotoDefinitionRequest, *},
27 util::resolve_iri,
28 Started,
29};
30use swls_lang_rdf_base::register_rdf_lang;
31use swls_lang_turtle::lang::parser::TurtleParseError;
32
33pub mod ecs;
34use crate::{
35 ecs::{
36 derive_jsonld_triples, format_jsonld_system, setup_completion, setup_parsing, ContextCache,
37 JsonLdActiveContext,
38 },
39 fs::build_registry,
40};
41
42#[derive(Component, Default)]
43pub struct JsonLdLang;
44
45#[derive(Debug, Default)]
46pub struct JsonLdHelper;
47
48impl LangHelper for JsonLdHelper {
49 fn keyword(&self) -> &[&'static str] {
50 &[
51 "@context",
52 "@id",
53 "@type",
54 "@graph",
55 "@base",
56 "@vocab",
57 "@language",
58 "@value",
59 "@list",
60 "@set",
61 "@reverse",
62 "@index",
63 "@container",
64 ]
65 }
66
67 fn default_position(&self) -> TripleTarget {
68 TripleTarget::Predicate
69 }
70
71 fn unquote<'a>(&self, text: &'a str) -> &'a str {
72 let s = text.strip_prefix('"').unwrap_or(text);
73 s.strip_suffix('"').unwrap_or(s)
74 }
75 fn quote(&self, inp: &str) -> String {
76 format!("\"{}\"", inp)
77 }
78 fn rename_placeholder<'a>(&self, raw: &'a str) -> &'a str {
79 self.unquote(raw)
80 }
81 fn rename_wrap(&self, new_text: &str) -> String {
82 self.quote(new_text)
83 }
84 fn handles_prefix_completion(&self) -> bool {
85 true
86 }
87
88 fn supports_prefix_diagnostics(&self) -> bool {
98 false
99 }
100
101 fn prefix_name_at<'a>(&self, source: &'a str, offset: usize) -> Option<&'a str> {
106 let bytes = source.as_bytes();
107 if offset == 0 || offset > source.len() || bytes[offset - 1] != b':' {
108 return None;
109 }
110 let colon = offset - 1;
111
112 let mut start = colon;
113 while start > 0 {
114 let c = bytes[start - 1];
115 if c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'.') {
116 start -= 1;
117 } else {
118 break;
119 }
120 }
121 if start == colon {
122 return None;
123 }
124 if start == 0 || bytes[start - 1] != b'"' {
128 return None;
129 }
130 Some(&source[start..colon])
131 }
132
133 fn prefix_edits(
134 &self,
135 source: &str,
136 rope: &LineIndex,
137 name: &str,
138 namespace: &str,
139 _format: swls_core::components::PrefixFormat,
140 ) -> Option<Vec<swls_core::lsp_types::TextEdit>> {
141 crate::ecs::completion::add_to_context(
142 source,
143 rope,
144 crate::ecs::completion::ContextEntry::Prefix { name, namespace },
145 )
146 }
147
148 fn inlay_types_hint(
149 &self,
150 subject: &Range<usize>,
151 rope: &LineIndex,
152 last_type: Option<&Range<usize>>,
153 types: Vec<Cow<'_, str>>,
154 ) -> Option<swls_core::lsp_types::InlayHint> {
155 let (label, position) = if let Some(lt) = last_type {
156 if let Some(pos) = offset_to_position(lt.end, &rope) {
157 let label = format!(", {}", types.join(", "));
158 (label, pos)
159 } else {
160 return None;
161 }
162 } else {
163 let offset = if rope.char_at_byte(subject.start) == Some('[') {
164 subject.start + 1
165 } else {
166 subject.end
167 };
168
169 if let Some(pos) = offset_to_position(offset + 1, &rope) {
170 let label = if types.len() == 1 {
171 format!(r#" "@type": "{}";"#, types[0])
172 } else {
173 format!(
174 r#" "@type": [ {} ],"#,
175 types
176 .into_iter()
177 .map(|x| format!("\"{}\"", x))
178 .collect::<Vec<_>>()
179 .join(", ")
180 )
181 };
182 (label, pos)
183 } else {
184 return None;
185 }
186 };
187
188 return Some(swls_core::lsp_types::InlayHint {
189 position,
190 label: swls_core::lsp_types::InlayHintLabel::String(label),
191 kind: None,
192 text_edits: None,
193 tooltip: None,
194 padding_left: None,
195 padding_right: None,
196 data: None,
197 });
198 }
199}
200
201pub fn setup_world<C: Client + ClientSync + Resource + Clone>(world: &mut World) {
202 register_rdf_lang::<JsonLdLang, JsonLdHelper>(world, &["jsonld"], &[".jsonld"]);
203
204 world.add_observer(
207 |trigger: On<CreateEvent>, mut commands: Commands, query: Query<&Source>| {
208 let e = trigger.event();
209 let is_json = trigger
210 .language_id
211 .as_ref()
212 .map(|l| l == "json")
213 .unwrap_or_default()
214 || e.url.as_str().ends_with(".json");
215 if !is_json {
216 return;
217 }
218 let entity = e.entity;
219 if let Ok(source) = query.get(entity) {
220 if source.0.contains("\"@context\"") {
221 commands
222 .entity(entity)
223 .insert(JsonLdLang::default())
224 .insert(DynLang(Box::new(JsonLdHelper::default())));
225 }
226 }
227 },
228 );
229 world.insert_resource(ContextCache::default());
230 world.insert_resource(Registry::empty());
231 setup_parsing::<C>(world);
232 setup_completion(world);
233
234 world.schedule_scope(FormatLabel, |_, schedule| {
235 schedule.add_systems(format_jsonld_system);
236 });
237
238 world.schedule_scope(Started, |_, schedule| {
239 schedule.add_systems((start_jsonld::<C>,));
240 });
241
242 world.schedule_scope(GotoDefinitionLabel, |_, schedule| {
243 schedule.add_systems(goto_cjs.after(swls_core::systems::get_current_prefix));
244 });
245}
246
247fn byte_offset_to_position(source: &str, offset: usize) -> swls_core::lsp_types::Position {
249 let offset = offset.min(source.len());
250 let before = &source[..offset];
251 let line = before.matches('\n').count() as u32;
252 let col = before.rfind('\n').map(|p| offset - p - 1).unwrap_or(offset) as u32;
253 swls_core::lsp_types::Position::new(line, col)
254}
255
256fn span_to_lsp_range(source: &str, span: &std::ops::Range<usize>) -> swls_core::lsp_types::Range {
258 swls_core::lsp_types::Range::new(
259 byte_offset_to_position(source, span.start),
260 byte_offset_to_position(source, span.end),
261 )
262}
263
264fn expand_iri_with_context(
269 active: &rdf_parsers::jsonld::convert::ActiveContext,
270 value: &str,
271) -> String {
272 expand_iri_inner(active, value, 0)
273}
274
275fn expand_iri_inner(
276 active: &rdf_parsers::jsonld::convert::ActiveContext,
277 value: &str,
278 depth: usize,
279) -> String {
280 if depth > 10 || value.is_empty() || value.starts_with('@') {
281 return value.to_string();
282 }
283 if value.starts_with("https://")
285 || value.starts_with("http://")
286 || value.starts_with("file://")
287 || value.starts_with("urn:")
288 {
289 return value.to_string();
290 }
291 if let Some(def) = active.terms.get(value) {
293 if let Some(iri) = &def.iri {
294 if iri != value {
295 return expand_iri_inner(active, iri, depth + 1);
296 }
297 }
298 }
299 if let Some(colon_pos) = value.find(':') {
301 if colon_pos > 0 {
302 let prefix = &value[..colon_pos];
303 let suffix = &value[colon_pos + 1..];
304 if let Some(def) = active.terms.get(prefix) {
305 if let Some(iri) = &def.iri {
306 let expanded_prefix = expand_iri_inner(active, iri, depth + 1);
307 return format!("{}{}", expanded_prefix, suffix);
308 }
309 }
310 }
311 }
312 value.to_string()
313}
314
315#[tracing::instrument(skip(query, res))]
316fn goto_cjs(
317 mut query: Query<
318 (
319 &TokenComponent,
320 Option<&TripleComponent>,
321 &Label,
322 &mut GotoDefinitionRequest,
323 Option<&JsonLdActiveContext>,
324 Option<&PrefixComponent>,
325 ),
326 With<JsonLdLang>,
327 >,
328 res: Res<Registry>,
329 config: Res<ServerConfig>,
330) {
331 use swls_core::lsp_types::{Location, Range};
332
333 if config.config.local.is_disabled(Disabled::GotoDefinitionComponentsJs) {
334 return;
335 }
336
337 for (token, triple, label, mut req, active_ctx, prefix) in &mut query {
338 if prefix.map(|p| p.is_namespace()).unwrap_or(false) {
343 continue;
344 }
345 let triple_term_str = triple.and_then(|tc| {
350 let term_span = match tc.target {
351 TripleTarget::Subject => &tc.triple.subject.span,
352 TripleTarget::Predicate => &tc.triple.predicate.span,
353 TripleTarget::Object => &tc.triple.object.span,
354 TripleTarget::Graph => return None,
355 };
356 let cursor = token.source_span.start;
357 if term_span.start <= cursor && cursor <= term_span.end {
358 tc.term().map(|t| t.as_str())
359 } else {
360 None
361 }
362 });
363 let raw_token = token.text.as_str().trim_matches('"');
364 let context_expanded = if triple_term_str.is_none() {
368 active_ctx.map(|ctx| expand_iri_with_context(&ctx.0, raw_token))
369 } else {
370 None
371 };
372 let st: &str = triple_term_str
373 .as_deref()
374 .or(context_expanded.as_deref())
375 .unwrap_or(raw_token);
376
377 tracing::debug!("Goto definition {:?} {}", triple_term_str, st,);
378
379 let found_target = if let Some(component) = res.0.components.get(st) {
381 Some((component.source_file.as_str(), component.iri_span.clone()))
382 } else if let Some(module) = res.0.modules.get(st) {
383 Some((module.source_file.as_str(), module.iri_span.clone()))
384 } else if let Some((file, span)) = res.0.parameters.get(st) {
385 Some((file.as_str(), span.clone()))
386 } else {
387 None
388 };
389
390 tracing::debug!(
391 "CJS from {:?} {:?}",
392 found_target,
393 resolve_iri_to_url(st, &res.1.import_paths),
394 );
395 if let Some((file, span)) = found_target {
396 if let Ok(uri) = swls_core::lsp_types::Url::parse(file) {
397 let range = res
398 .0
399 .file_sources
400 .get(file)
401 .map(|src| span_to_lsp_range(src, &span))
402 .unwrap_or_default();
403 req.0.push(Location { uri, range });
404 continue;
405 }
406 }
407
408 let iri_no_fragment = st.split('#').next().unwrap_or(st);
409 let resolved = resolve_iri_to_url(iri_no_fragment, &res.1.import_paths)
410 .or_else(|| res.1.context_urls.get(iri_no_fragment).cloned());
411 if let Some(t) = resolved {
412 tracing::debug!("target {}", t.as_str());
413 req.0.push(Location {
414 uri: t,
415 range: Range::default(),
416 });
417 continue;
418 }
419
420 if triple_term_str.is_none() {
421 let target = resolve_iri(&label.as_str(), st);
423 if let Ok(uri) = swls_core::lsp_types::Url::parse(&target) {
424 req.0.push(Location {
425 uri,
426 range: Range::default(),
427 });
428 continue;
429 }
430 }
431
432 tracing::debug!("goto_cjs: no definition found for '{}'", st);
433 }
434}
435
436mod fs {
437 use components_rs::{
438 error::{ComponentsJsError, Result},
439 fs::FsDirEntry,
440 };
441 use swls_core::{lsp_types::Url, prelude::Fs};
442
443 use crate::Registry;
444
445 pub struct LocalFs(Fs);
446
447 #[async_trait::async_trait]
448 impl components_rs::fs::Fs for LocalFs {
449 async fn read_to_string(&self, url: &Url) -> Result<String> {
451 self.0
452 .0
453 .read_file(&url)
454 .await
455 .ok_or(ComponentsJsError::General(format!(
456 "Failed to read file {}",
457 url.as_str()
458 )))
459 }
460
461 async fn read_dir(&self, path: &Url) -> Result<Vec<components_rs::fs::FsDirEntry>> {
463 let entries = self
464 .0
465 .0
466 .read_dir(path)
467 .await
468 .ok_or(ComponentsJsError::General(format!(
469 "Failed to read dir {:?}",
470 path.as_str()
471 )))?;
472 Ok(entries
473 .into_iter()
474 .map(|entry| components_rs::fs::FsDirEntry {
475 name: entry.name,
476 path: entry.path,
477 is_dir: entry.is_dir,
478 })
479 .collect())
480 }
481
482 async fn is_file(&self, path: &Url) -> bool {
484 self.0 .0.is_file(path).await
485 }
486
487 async fn is_dir(&self, path: &Url) -> bool {
489 self.0 .0.is_dir(path).await
490 }
491
492 async fn glob(&self, base: &Url, pattern: &str) -> Result<Vec<FsDirEntry>> {
493 let entries = self
494 .0
495 .0
496 .glob(base, pattern)
497 .await
498 .ok_or(ComponentsJsError::General(format!(
499 "Failed to read dir {:?} {}",
500 base.as_str(),
501 pattern
502 )))?;
503 Ok(entries
504 .into_iter()
505 .map(|entry| components_rs::fs::FsDirEntry {
506 name: entry.name,
507 path: entry.path,
508 is_dir: entry.is_dir,
509 })
510 .collect())
511 }
512 }
513
514 pub async fn build_registry(fs: &Fs, path: &Url) -> Result<Registry> {
515 use components_rs::components::registry::ComponentRegistry;
516 use components_rs::module_state::ModuleState;
517
518 let fs = LocalFs(fs.clone());
519 let state = ModuleState::build(&fs, path).await?;
520
521 let mut registry = ComponentRegistry::new();
522 registry.register_available_modules(&fs, &state).await?;
523 registry.finalize();
524
525 Ok(Registry(registry, state))
526 }
527}
528
529fn build_cjs_quads(registry: &ComponentRegistry) -> Vec<Quad> {
530 let rdf_type = NamedNode::new_unchecked("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
531 let rdfs_class = NamedNode::new_unchecked("http://www.w3.org/2000/01/rdf-schema#Class");
532 let rdfs_subclass_of =
533 NamedNode::new_unchecked("http://www.w3.org/2000/01/rdf-schema#subClassOf");
534 let rdfs_comment = NamedNode::new_unchecked("http://www.w3.org/2000/01/rdf-schema#comment");
535 let rdf_property =
536 NamedNode::new_unchecked("http://www.w3.org/1999/02/22-rdf-syntax-ns#Property");
537 let rdfs_domain = NamedNode::new_unchecked("http://www.w3.org/2000/01/rdf-schema#domain");
538 let rdfs_range = NamedNode::new_unchecked("http://www.w3.org/2000/01/rdf-schema#range");
539
540 let graph = GraphName::DefaultGraph;
541 let mut quads = Vec::new();
542
543 for comp in registry.components.values() {
544 let iri = NamedNode::new_unchecked(&comp.iri);
545
546 quads.push(Quad::new(
547 iri.clone(),
548 rdf_type.clone(),
549 rdfs_class.clone(),
550 graph.clone(),
551 ));
552
553 if let Some(comment) = &comp.comment {
554 quads.push(Quad::new(
555 iri.clone(),
556 rdfs_comment.clone(),
557 Literal::new_simple_literal(comment.as_str()),
558 graph.clone(),
559 ));
560 }
561
562 for parent in &comp.extends {
563 let parent_node = NamedNode::new_unchecked(parent);
564 quads.push(Quad::new(
565 iri.clone(),
566 rdfs_subclass_of.clone(),
567 parent_node,
568 graph.clone(),
569 ));
570 }
571
572 for param in &comp.parameters {
573 let param_iri = NamedNode::new_unchecked(¶m.iri);
574
575 quads.push(Quad::new(
576 param_iri.clone(),
577 rdf_type.clone(),
578 rdf_property.clone(),
579 graph.clone(),
580 ));
581 quads.push(Quad::new(
582 param_iri.clone(),
583 rdfs_domain.clone(),
584 iri.clone(),
585 graph.clone(),
586 ));
587
588 if let Some(range) = ¶m.range {
589 let range_node = NamedNode::new_unchecked(range);
590 quads.push(Quad::new(
591 param_iri.clone(),
592 rdfs_range.clone(),
593 range_node,
594 graph.clone(),
595 ));
596 }
597
598 if let Some(comment) = ¶m.comment {
599 quads.push(Quad::new(
600 param_iri.clone(),
601 rdfs_comment.clone(),
602 Literal::new_simple_literal(comment.as_str()),
603 graph.clone(),
604 ));
605 }
606 }
607 }
608
609 quads
610}
611
612#[derive(Resource)]
613pub struct Registry(pub ComponentRegistry, pub ModuleState);
614impl Registry {
615 pub fn empty() -> Self {
616 Self(ComponentRegistry::new(), ModuleState::empty())
617 }
618}
619
620#[tracing::instrument(skip(fs, client, config, commands))]
621fn start_jsonld<C: Client + Resource + Clone>(
622 fs: Res<Fs>,
623 client: Res<C>,
624 config: Res<ServerConfig>,
625 commands: Res<CommandSender>,
626) {
627 if !config.config.jsonld.unwrap_or(true) {
628 return;
629 }
630 let fs = fs.clone();
631 tracing::debug!("loading CJS registry, config: {:?}", config);
632 if let Some(ws) = config.workspaces.first().and_then(|x| {
633 if x.uri.as_str().ends_with('/') {
634 Some(x.uri.clone())
635 } else {
636 Url::parse(&format!("{}/", x.uri.as_str())).ok()
637 }
638 }) {
639 tracing::debug!("CJS workspace root: {:?}", ws.as_str());
640 let commands = commands.clone();
641 let thing = async move {
642 tracing::debug!("Starting CJS registry build for {:?}", ws.as_str());
643 if let Ok(reg) = build_registry(&fs, &ws).await {
644 let mut command_queue = CommandQueue::default();
645 command_queue.push(move |world: &mut World| {
646 let quads = build_cjs_quads(®.0);
647 world.insert_resource(reg);
648
649 let store_clone = world.get_resource::<swls_core::store::Store>();
650 if let Some(store) = store_clone {
652 tracing::debug!("Derive store found adding {} triples", quads.len());
653 let mut loader = store.0.bulk_loader();
654 let _ = loader.load_quads(quads.into_iter());
655 let _ = loader.commit();
656 }
657
658 let _ = world.run_system_once(derive_jsonld_triples::<C>);
659 });
660 let _ = commands.unbounded_send(command_queue);
661 }
662 ()
663 };
664 client.spawn(thing);
665 } else {
666 tracing::warn!("No workspace root found, skipping CJS registry build");
667 }
668}
669
670impl Lang for JsonLdLang {
671 type ElementError = TurtleParseError;
672
673 const LANG: &'static str = "jsonld";
674 const TRIGGERS: &'static [&'static str] = &["\"@", "\""];
675 const CODE_ACTION: bool = false;
676 const HOVER: bool = true;
677 const PATTERN: Option<&'static str> = None;
678
679 const LEGEND_TYPES: &'static [SemanticTokenType] = &[
680 semantic_token::BOOLEAN,
681 SemanticTokenType::COMMENT,
682 SemanticTokenType::ENUM_MEMBER,
683 SemanticTokenType::KEYWORD,
684 SemanticTokenType::NAMESPACE,
685 SemanticTokenType::NUMBER,
686 SemanticTokenType::PROPERTY,
687 SemanticTokenType::STRING,
688 ];
689
690 fn semantic_token_type(kind: rowan::SyntaxKind) -> Option<SemanticTokenType> {
691 use rdf_parsers::jsonld::parser::SyntaxKind as SK;
692 let k = kind.0;
693 if k == SK::Comment as u16 {
694 Some(SemanticTokenType::COMMENT)
695 } else if k == SK::StringToken as u16 {
696 Some(SemanticTokenType::STRING)
697 } else if k == SK::JsonNumber as u16 {
698 Some(SemanticTokenType::NUMBER)
699 } else if k == SK::TrueLit as u16 || k == SK::FalseLit as u16 || k == SK::NullLit as u16 {
700 Some(semantic_token::BOOLEAN)
701 } else {
702 None
703 }
704 }
705
706 fn semantic_token_spans(
707 kind: rowan::SyntaxKind,
708 span: std::ops::Range<usize>,
709 text: &str,
710 ) -> Vec<(SemanticTokenType, std::ops::Range<usize>)> {
711 if text.get(span.start + 1..span.start + 2) == Some("@") {
713 return vec![(SemanticTokenType::KEYWORD, span)];
714 }
715 Self::semantic_token_type(kind)
722 .map(|t| vec![(t, span)])
723 .unwrap_or_default()
724 }
725}