1pub mod code_actions;
2pub mod parse;
3pub mod rename;
4pub mod traits;
5pub mod triples;
6
7use bevy_ecs::{
8 component::Component, event::EntityEvent, observer::On, prelude::Resource, system::Commands,
9 world::World,
10};
11use swls_core::{
12 feature::{
13 diagnostics::publish_diagnostics,
14 semantic::{basic_semantic_tokens, semantic_tokens_system},
15 },
16 lang::{Lang, LangHelper},
17 prelude::*,
18 CreateEvent,
19};
20
21pub fn register_rdf_lang<L, H>(
33 world: &mut World,
34 language_id: &'static [&'static str],
35 extensions: &'static [&'static str],
36) where
37 L: Lang + Component + Default + Send + Sync + 'static,
38 L::ElementError: 'static + Clone,
39 H: LangHelper + Default + Send + Sync + 'static,
40{
41 ensure_shared_rdf_systems(world);
42
43 let mut semantic_token_dict = world.resource_mut::<SemanticTokensDict>();
44 L::LEGEND_TYPES.iter().for_each(|lt| {
45 if !semantic_token_dict.contains_key(lt) {
46 let l = semantic_token_dict.0.len();
47 semantic_token_dict.insert(lt.clone(), l);
48 }
49 });
50
51 world.add_observer(move |trigger: On<CreateEvent>, mut commands: Commands| {
52 let e = trigger.event();
53 let matches = trigger
54 .language_id
55 .as_ref()
56 .map(|lang_id| language_id.iter().any(|x| x == lang_id))
57 .unwrap_or_default()
58 || extensions.iter().any(|ext| e.url.as_str().ends_with(ext));
59 if matches {
60 commands
61 .entity(e.event_target())
62 .insert(L::default())
63 .insert(DynLang(Box::new(H::default())));
64 }
65 });
66
67 world.schedule_scope(swls_core::feature::ParseLabel, |_, schedule| {
68 use bevy_ecs::schedule::IntoScheduleConfigs;
69 schedule.add_systems(publish_diagnostics::<L>.after(swls_core::feature::parse::end));
70 });
71
72 world.schedule_scope(swls_core::feature::SemanticLabel, |_, schedule| {
73 use bevy_ecs::schedule::IntoScheduleConfigs;
74 schedule.add_systems((
75 basic_semantic_tokens::<L>.before(semantic_tokens_system),
76 semantic_tokens::<L>
77 .after(basic_semantic_tokens::<L>)
78 .before(semantic_tokens_system),
79 ));
80 });
81}
82
83#[derive(Resource)]
84struct SharedRdfSystemsRegistered;
85
86fn ensure_shared_rdf_systems(world: &mut World) {
96 if world.contains_resource::<SharedRdfSystemsRegistered>() {
97 return;
98 }
99 world.insert_resource(SharedRdfSystemsRegistered);
100
101 world.schedule_scope(swls_core::feature::code_action::Label, |_, schedule| {
102 schedule.add_systems((
103 code_actions::extract_blank_node,
104 code_actions::inline_blank_node,
105 ));
106 });
107 world.schedule_scope(swls_core::feature::rename::PrepareRename, |_, schedule| {
108 schedule.add_systems(rename::prepare_rename);
109 });
110 world.schedule_scope(swls_core::feature::rename::Rename, |_, schedule| {
111 schedule.add_systems(rename::rename);
112 });
113}
114
115pub use tokens::semantic_tokens;
116mod tokens {
117 use bevy_ecs::prelude::*;
118 use rdf_parsers::model::{BlankNode, Literal, NamedNode, RDFLiteral, Term};
119 use swls_core::lsp_types::SemanticTokenType;
120 use swls_core::prelude::semantic::*;
121 use swls_core::prelude::*;
122
123 fn span_is_nested_object(span: &std::ops::Range<usize>, source: &str) -> bool {
127 source
128 .get(span.clone())
129 .map(|s| s.trim_start().starts_with('{'))
130 .unwrap_or(false)
131 }
132
133 fn add_term(
134 term: &Spanned<Term>,
135 ttc: &mut TokenTypesComponent,
136 kind: SemanticTokenType,
137 source: &str,
138 ) {
139 if source
143 .get(term.span().clone())
144 .is_some_and(|s| s.trim_start_matches('"').starts_with('@'))
145 {
146 return;
147 }
148 match term.value() {
149 Term::NamedNode(NamedNode::Prefixed {
150 prefix, value, idx, ..
151 }) => {
152 let start = *idx;
153 if source.as_bytes().get(start) == Some(&b'"') && !value.is_empty() {
154 let sep = start + prefix.len() + 2; let close = sep + value.len(); ttc.push(spanned(kind, start..close + 1));
163 ttc.push(spanned(SemanticTokenType::NAMESPACE, start + 1..sep));
164 } else if source.as_bytes().get(start) == Some(&b'"') {
165 ttc.push(spanned(kind, term.span().clone()));
168 } else {
169 let sep = start + prefix.len() + 1; ttc.push(spanned(kind, sep..sep + value.len()));
173 }
174 }
175 Term::Variable(_) | Term::NamedNode(_) => {
176 if span_is_nested_object(term.span(), source) {
182 return;
183 }
184 ttc.push(spanned(kind, term.span().clone()));
185 }
186 Term::BlankNode(BlankNode::Named(_, _)) => return,
192 Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
193 for po in pos {
194 for o in &po.object {
195 add_term(o, ttc, kind.clone(), source);
196 }
197 }
198 }
199 Term::Collection(spanneds) => {
200 for e in spanneds {
201 add_term(e, ttc, kind.clone(), source);
202 }
203 }
204 Term::Literal(Literal::RDF(RDFLiteral { ty: Some(la), .. })) => {
205 let dt = la.as_ref().map(|x| Term::NamedNode(x.clone()));
206 add_term(&dt, ttc, kind, source);
207 }
208 _ => return,
209 }
210 }
211
212 pub fn semantic_tokens<L: Component>(
213 query: Query<
214 (&Element, &Source, &mut TokenTypesComponent),
215 (With<L>, With<HighlightRequest>),
216 >,
217 ) {
218 for (turtle, source, mut ttc) in query {
219 let source = source.0.as_str();
220 for p in &turtle.prefixes {
225 let span = p.value().prefix.span();
226 if source.as_bytes().get(span.start) == Some(&b'"') {
227 ttc.push(spanned(SemanticTokenType::NAMESPACE, span.clone()));
228 }
229 }
230 for t in &turtle.triples {
231 add_term(&t.subject, &mut ttc, SemanticTokenType::ENUM_MEMBER, source);
232 for po in &t.po {
233 add_term(
234 &po.predicate,
235 &mut ttc,
236 SemanticTokenType::ENUM_MEMBER,
237 source,
238 );
239 for o in &po.object {
240 add_term(o, &mut ttc, SemanticTokenType::ENUM_MEMBER, source);
241 }
242 }
243 }
244 }
245 }
246
247 #[cfg(test)]
248 mod tests {
249 use super::*;
250
251 fn full(iri: &str, offset: usize, span: std::ops::Range<usize>) -> Spanned<Term> {
252 spanned(Term::NamedNode(NamedNode::Full(iri.into(), offset)), span)
253 }
254
255 #[test]
259 fn nested_named_object_is_skipped() {
260 let source = r#"{ "@id": "http://ex/x", "name": "n" }"#;
261 let mut ttc: TokenTypesComponent = Wrapped(Vec::new());
262 add_term(
264 &full("http://ex/x", 9, 0..source.len()),
265 &mut ttc,
266 SemanticTokenType::ENUM_MEMBER,
267 source,
268 );
269 assert!(ttc.0.is_empty(), "nested {{ }} object should be skipped");
270
271 let id_src = r#""http://ex/x""#;
273 add_term(
274 &full("http://ex/x", 0, 0..id_src.len()),
275 &mut ttc,
276 SemanticTokenType::ENUM_MEMBER,
277 id_src,
278 );
279 assert_eq!(ttc.0.len(), 1, "plain IRI reference should be stamped");
280 }
281 }
282}