lsp_core/systems/
properties.rs

1use std::{borrow::Cow, collections::HashSet};
2
3use bevy_ecs::prelude::*;
4use completion::{CompletionRequest, SimpleCompletion};
5use hover::HoverRequest;
6use sophia_api::{
7    ns::rdfs,
8    prelude::{Any, Dataset},
9    quad::Quad,
10    term::Term,
11};
12use systems::OntologyExtractor;
13use tracing::{debug, info, instrument};
14
15use crate::{
16    lsp_types::{CompletionItemKind, TextEdit},
17    prelude::*,
18    util::{ns::*, triple::MyTerm},
19};
20
21#[derive(PartialEq, Eq, Hash)]
22pub struct DefinedClass {
23    pub term: MyTerm<'static>,
24    pub label: String,
25    pub comment: String,
26    pub reason: &'static str,
27    pub location: std::ops::Range<usize>,
28}
29
30pub type DefinedClasses = HashSet<DefinedClass>;
31
32fn derive_class(
33    subject: <MyTerm<'_> as Term>::BorrowTerm<'_>,
34    triples: &Triples,
35    source: &'static str,
36) -> Option<DefinedClass> {
37    let label = triples
38        .object([subject], [rdfs::label, dc::title])?
39        .to_owned()
40        .as_str()
41        .to_string();
42    let comment = triples
43        .object([subject], [rdfs::comment, dc::description])?
44        .to_owned()
45        .as_str()
46        .to_string();
47    Some(DefinedClass {
48        label,
49        comment,
50        term: subject.to_owned(),
51        reason: source,
52        location: subject.span.clone(),
53    })
54}
55
56pub fn derive_classes(
57    query: Query<(Entity, &Triples, &Label), (Changed<Triples>, Without<Dirty>)>,
58    mut commands: Commands,
59    extractor: Res<OntologyExtractor>,
60) {
61    for (e, triples, label) in &query {
62        let classes: HashSet<_> = triples
63            .0
64            .quads_matching(Any, [rdf::type_], extractor.classes(), Any)
65            .flatten()
66            .flat_map(|x| derive_class(x.s(), &triples, "owl_class"))
67            .collect();
68
69        info!(
70            "({} classes) Found {} classes for {} ({} triples)",
71            extractor.classes().len(),
72            classes.len(),
73            label.0,
74            triples.0.len()
75        );
76        commands.entity(e).insert(Wrapped(classes));
77    }
78}
79
80#[instrument(skip(query, other))]
81pub fn complete_class(
82    mut query: Query<(
83        &TokenComponent,
84        &TripleComponent,
85        &Prefixes,
86        &DocumentLinks,
87        &Label,
88        &mut CompletionRequest,
89    )>,
90    other: Query<(&Label, &Wrapped<DefinedClasses>)>,
91) {
92    for (token, triple, prefixes, links, this_label, mut request) in &mut query {
93        if triple.triple.predicate.value == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
94            && triple.target == TripleTarget::Object
95        {
96            for (label, classes) in &other {
97                // Check if this thing is actually linked
98                if links
99                    .iter()
100                    .find(|link| link.0.as_str().starts_with(label.0.as_str()))
101                    .is_none()
102                    && label.0 != this_label.0
103                {
104                    debug!(
105                        "Not looking for defined classes in {} (not linked)",
106                        label.0
107                    );
108                    continue;
109                }
110
111                let st = classes.0.iter().take(5).fold(String::new(), |mut st, b| {
112                    if let Some(short) = prefixes.shorten(&b.term.value) {
113                        st += &short;
114                    } else {
115                        st += &b.term.value;
116                    }
117                    st += ", ";
118                    st
119                });
120                debug!(
121                    "Looking for defined classes in {} (found {} {})",
122                    label.0,
123                    classes.0.len(),
124                    st
125                );
126
127                for class in classes.0.iter() {
128                    let to_beat = prefixes
129                        .shorten(&class.term.value)
130                        .map(|x| Cow::Owned(x))
131                        .unwrap_or(class.term.value.clone());
132
133                    if to_beat.starts_with(&token.text) {
134                        request.push(
135                            SimpleCompletion::new(
136                                CompletionItemKind::CLASS,
137                                format!("{}", to_beat),
138                                TextEdit {
139                                    range: token.range.clone(),
140                                    new_text: to_beat.to_string(),
141                                },
142                            )
143                            .documentation(&class.comment),
144                        );
145                    }
146                }
147            }
148        }
149    }
150}
151
152pub fn hover_class(
153    mut query: Query<(
154        &TokenComponent,
155        &Prefixes,
156        &DocumentLinks,
157        &mut HoverRequest,
158    )>,
159    other: Query<(&Label, &Wrapped<DefinedClasses>)>,
160) {
161    for (token, prefixes, links, mut request) in &mut query {
162        if let Some(target) = prefixes.expand(token.token.value()) {
163            for (label, classes) in &other {
164                // Check if this thing is actually linked
165                if links.iter().find(|link| link.0 == label.0).is_none() {
166                    continue;
167                }
168
169                for c in classes.iter().filter(|c| c.term.value == target) {
170                    request.0.push(format!("{}: {}", c.label, c.comment));
171                }
172            }
173        }
174    }
175}
176
177#[derive(PartialEq, Eq, Hash)]
178pub struct DefinedProperty {
179    pub predicate: MyTerm<'static>,
180    pub comment: String,
181    pub label: String,
182    pub range: Vec<String>,
183    pub domain: Vec<String>,
184    pub reason: &'static str,
185}
186pub type DefinedProperties = HashSet<DefinedProperty>;
187
188fn derive_property(
189    subject: <MyTerm<'_> as Term>::BorrowTerm<'_>,
190    triples: &Triples,
191    source: &'static str,
192) -> Option<DefinedProperty> {
193    let label = triples
194        .object([subject], [rdfs::label, dc::title])?
195        .to_owned()
196        .as_str()
197        .to_string();
198    let comment = triples
199        .object([subject], [rdfs::comment, dc::description])?
200        .to_owned()
201        .as_str()
202        .to_string();
203    let domain: Vec<_> = triples
204        .objects([subject], [rdfs::domain])
205        .map(|x| x.as_str().to_string())
206        .collect();
207
208    let range: Vec<_> = triples
209        .objects([subject], [rdfs::range])
210        .map(|x| x.as_str().to_string())
211        .collect();
212
213    Some(DefinedProperty {
214        predicate: subject.to_owned(),
215        range,
216        domain,
217        label,
218        comment,
219        reason: source,
220    })
221}
222
223pub fn derive_properties(
224    query: Query<(Entity, &Triples, &Label), (Changed<Triples>, Without<Dirty>)>,
225    mut commands: Commands,
226    extractor: Res<OntologyExtractor>,
227) {
228    for (e, triples, label) in &query {
229        let classes: HashSet<_> = triples
230            .0
231            .quads_matching(Any, [rdf::type_], extractor.properties(), Any)
232            .flatten()
233            .flat_map(|x| derive_property(x.s(), &triples, "owl_property"))
234            .collect();
235
236        info!(
237            "({} properties) Found {} properties for {} ({} triples)",
238            extractor.properties().len(),
239            classes.len(),
240            label.0.as_str(),
241            triples.0.len()
242        );
243        commands.entity(e).insert(Wrapped(classes));
244    }
245}
246
247#[instrument(skip(query, other, hierarchy))]
248pub fn complete_properties(
249    mut query: Query<(
250        &TokenComponent,
251        &TripleComponent,
252        &Prefixes,
253        &DocumentLinks,
254        &Label,
255        &Types,
256        &mut CompletionRequest,
257    )>,
258    other: Query<(&Label, &Wrapped<DefinedProperties>)>,
259    hierarchy: Res<TypeHierarchy<'static>>,
260) {
261    debug!("Complete properties");
262    for (token, triple, prefixes, links, this_label, types, mut request) in &mut query {
263        debug!("target {:?} text {}", triple.target, token.text);
264        debug!("links {:?}", links);
265        if triple.target == TripleTarget::Predicate {
266            let tts = types.get(&triple.triple.subject.value);
267            for (label, properties) in &other {
268                // Check if this thing is actually linked
269                if links
270                    .iter()
271                    .find(|link| link.0.as_str().starts_with(label.0.as_str()))
272                    .is_none()
273                    && label.0 != this_label.0
274                {
275                    debug!("This link is ignored {}", label.as_str());
276                    continue;
277                }
278
279                for class in properties.0.iter() {
280                    let to_beat = prefixes
281                        .shorten(&class.predicate.value)
282                        .map(|x| Cow::Owned(x))
283                        .unwrap_or(class.predicate.value.clone());
284
285                    debug!(
286                        "{} starts with {} = {}",
287                        to_beat,
288                        token.text,
289                        to_beat.starts_with(&token.text)
290                    );
291
292                    if to_beat.starts_with(&token.text) {
293                        let correct_domain = class.domain.iter().any(|domain| {
294                            if let Some(domain_id) = hierarchy.get_id_ref(&domain) {
295                                if let Some(tts) = tts {
296                                    tts.iter().any(|tt| *tt == domain_id)
297                                } else {
298                                    false
299                                }
300                            } else {
301                                false
302                            }
303                        });
304
305                        let mut completion = SimpleCompletion::new(
306                            CompletionItemKind::PROPERTY,
307                            format!("{}", to_beat),
308                            TextEdit {
309                                range: token.range.clone(),
310                                new_text: to_beat.to_string(),
311                            },
312                        )
313                        .label_description(&class.comment);
314
315                        if correct_domain {
316                            completion.kind = CompletionItemKind::FIELD;
317                            debug!("Property has correct domain {}", to_beat);
318                            request.push(completion.sort_text("1"));
319                        } else {
320                            request.push(completion);
321                        }
322                    }
323                }
324            }
325        }
326    }
327}
328
329#[instrument(skip(query, other))]
330pub fn hover_property(
331    mut query: Query<(
332        &TokenComponent,
333        &Prefixes,
334        &DocumentLinks,
335        &mut HoverRequest,
336    )>,
337    other: Query<(&Label, Option<&Prefixes>, &Wrapped<DefinedProperties>)>,
338) {
339    for (token, prefixes, links, mut request) in &mut query {
340        if let Some(target) = prefixes.expand(token.token.value()) {
341            for (label, p2, classes) in &other {
342                // Check if this thing is actually linked
343                if links.iter().find(|link| link.0 == label.0).is_none() {
344                    continue;
345                }
346
347                let shorten = |from: &str| {
348                    if let Some(x) = prefixes.shorten(from) {
349                        return Some(x);
350                    }
351
352                    if let Some(p) = p2 {
353                        return p.shorten(from);
354                    }
355
356                    None
357                };
358
359                for c in classes.iter().filter(|c| c.predicate.value == target) {
360                    request.0.push(format!("{}: {}", c.label, c.comment));
361                    for r in &c.range {
362                        let range = shorten(&r);
363                        request.0.push(format!(
364                            "Range {}",
365                            range.as_ref().map(|x| x.as_str()).unwrap_or(r.as_str())
366                        ));
367                    }
368
369                    for d in &c.domain {
370                        let domain = shorten(&d);
371                        request.0.push(format!(
372                            "Domain {}",
373                            domain.as_ref().map(|x| x.as_str()).unwrap_or(d.as_str())
374                        ));
375                    }
376                }
377            }
378        }
379    }
380}