lsp_core/systems/
properties.rs

1use std::{borrow::Cow, collections::HashSet};
2
3use bevy_ecs::prelude::*;
4use completion::{CompletionRequest, SimpleCompletion};
5use hover::HoverRequest;
6use lsp_types::{CompletionItemKind, TextEdit};
7use sophia_api::{
8    ns::rdfs,
9    prelude::{Any, Dataset},
10    quad::Quad,
11    term::Term,
12};
13use systems::OntologyExtractor;
14use tracing::{debug, info, instrument};
15
16use crate::{
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])?
39        .to_owned()
40        .as_str()
41        .to_string();
42    let comment = triples
43        .object([subject], [rdfs::comment])?
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                debug!("Looking for defined classes in {}", label.0);
111
112                for class in classes.0.iter() {
113                    let to_beat = prefixes
114                        .shorten(&class.term.value)
115                        .map(|x| Cow::Owned(x))
116                        .unwrap_or(class.term.value.clone());
117
118                    if to_beat.starts_with(&token.text) {
119                        request.push(
120                            SimpleCompletion::new(
121                                CompletionItemKind::CLASS,
122                                format!("{}", to_beat),
123                                TextEdit {
124                                    range: token.range.clone(),
125                                    new_text: to_beat.to_string(),
126                                },
127                            )
128                            .documentation(&class.comment),
129                        );
130                    }
131                }
132            }
133        }
134    }
135}
136
137pub fn hover_class(
138    mut query: Query<(
139        &TokenComponent,
140        &Prefixes,
141        &DocumentLinks,
142        &mut HoverRequest,
143    )>,
144    other: Query<(&Label, &Wrapped<DefinedClasses>)>,
145) {
146    for (token, prefixes, links, mut request) in &mut query {
147        if let Some(target) = prefixes.expand(token.token.value()) {
148            for (label, classes) in &other {
149                // Check if this thing is actually linked
150                if links.iter().find(|link| link.0 == label.0).is_none() {
151                    continue;
152                }
153
154                for c in classes.iter().filter(|c| c.term.value == target) {
155                    request.0.push(format!("{}: {}", c.label, c.comment));
156                }
157            }
158        }
159    }
160}
161
162#[derive(PartialEq, Eq, Hash)]
163pub struct DefinedProperty {
164    pub predicate: MyTerm<'static>,
165    pub comment: String,
166    pub label: String,
167    pub range: Vec<String>,
168    pub domain: Vec<String>,
169    pub reason: &'static str,
170}
171pub type DefinedProperties = HashSet<DefinedProperty>;
172
173fn derive_property(
174    subject: <MyTerm<'_> as Term>::BorrowTerm<'_>,
175    triples: &Triples,
176    source: &'static str,
177) -> Option<DefinedProperty> {
178    let label = triples
179        .object([subject], [rdfs::label])?
180        .to_owned()
181        .as_str()
182        .to_string();
183    let comment = triples
184        .object([subject], [rdfs::comment])?
185        .to_owned()
186        .as_str()
187        .to_string();
188    let domain: Vec<_> = triples
189        .objects([subject], [rdfs::domain])
190        .map(|x| x.as_str().to_string())
191        .collect();
192
193    let range: Vec<_> = triples
194        .objects([subject], [rdfs::range])
195        .map(|x| x.as_str().to_string())
196        .collect();
197
198    Some(DefinedProperty {
199        predicate: subject.to_owned(),
200        range,
201        domain,
202        label,
203        comment,
204        reason: source,
205    })
206}
207
208pub fn derive_properties(
209    query: Query<(Entity, &Triples, &Label), (Changed<Triples>, Without<Dirty>)>,
210    mut commands: Commands,
211    extractor: Res<OntologyExtractor>,
212) {
213    for (e, triples, label) in &query {
214        let classes: HashSet<_> = triples
215            .0
216            .quads_matching(Any, [rdf::type_], extractor.properties(), Any)
217            .flatten()
218            .flat_map(|x| derive_property(x.s(), &triples, "owl_property"))
219            .collect();
220
221        info!(
222            "({} properties) Found {} properties for {} ({} triples)",
223            extractor.properties().len(),
224            classes.len(),
225            label.0.as_str(),
226            triples.0.len()
227        );
228        commands.entity(e).insert(Wrapped(classes));
229    }
230}
231
232#[instrument(skip(query, other, hierarchy))]
233pub fn complete_properties(
234    mut query: Query<(
235        &TokenComponent,
236        &TripleComponent,
237        &Prefixes,
238        &DocumentLinks,
239        &Label,
240        &Types,
241        &mut CompletionRequest,
242    )>,
243    other: Query<(&Label, &Wrapped<DefinedProperties>)>,
244    hierarchy: Res<TypeHierarchy<'static>>,
245) {
246    debug!("Complete properties");
247    for (token, triple, prefixes, links, this_label, types, mut request) in &mut query {
248        debug!("target {:?} text {}", triple.target, token.text);
249        debug!("links {:?}", links);
250        if triple.target == TripleTarget::Predicate {
251            let tts = types.get(&triple.triple.subject.value);
252            for (label, properties) in &other {
253                // Check if this thing is actually linked
254                if links
255                    .iter()
256                    .find(|link| link.0.as_str().starts_with(label.0.as_str()))
257                    .is_none()
258                    && label.0 != this_label.0
259                {
260                    debug!("This link is ignored {}", label.as_str());
261                    continue;
262                }
263
264                for class in properties.0.iter() {
265                    let to_beat = prefixes
266                        .shorten(&class.predicate.value)
267                        .map(|x| Cow::Owned(x))
268                        .unwrap_or(class.predicate.value.clone());
269
270                    debug!(
271                        "{} starts with {} = {}",
272                        to_beat,
273                        token.text,
274                        to_beat.starts_with(&token.text)
275                    );
276
277                    if to_beat.starts_with(&token.text) {
278                        let correct_domain = class.domain.iter().any(|domain| {
279                            if let Some(domain_id) = hierarchy.get_id_ref(&domain) {
280                                if let Some(tts) = tts {
281                                    tts.iter().any(|tt| *tt == domain_id)
282                                } else {
283                                    false
284                                }
285                            } else {
286                                false
287                            }
288                        });
289
290                        let mut completion = SimpleCompletion::new(
291                            CompletionItemKind::PROPERTY,
292                            format!("{}", to_beat),
293                            TextEdit {
294                                range: token.range.clone(),
295                                new_text: to_beat.to_string(),
296                            },
297                        )
298                        .label_description(&class.comment);
299
300                        if correct_domain {
301                            completion.kind = CompletionItemKind::FIELD;
302                            debug!("Property has correct domain {}", to_beat);
303                            request.push(completion.sort_text("1"));
304                        } else {
305                            request.push(completion);
306                        }
307                    }
308                }
309            }
310        }
311    }
312}
313
314#[instrument(skip(query, other))]
315pub fn hover_property(
316    mut query: Query<(
317        &TokenComponent,
318        &Prefixes,
319        &DocumentLinks,
320        &mut HoverRequest,
321    )>,
322    other: Query<(&Label, Option<&Prefixes>, &Wrapped<DefinedProperties>)>,
323) {
324    for (token, prefixes, links, mut request) in &mut query {
325        if let Some(target) = prefixes.expand(token.token.value()) {
326            for (label, p2, classes) in &other {
327                // Check if this thing is actually linked
328                if links.iter().find(|link| link.0 == label.0).is_none() {
329                    continue;
330                }
331
332                let shorten = |from: &str| {
333                    if let Some(x) = prefixes.shorten(from) {
334                        return Some(x);
335                    }
336
337                    if let Some(p) = p2 {
338                        return p.shorten(from);
339                    }
340
341                    None
342                };
343
344                for c in classes.iter().filter(|c| c.predicate.value == target) {
345                    request.0.push(format!("{}: {}", c.label, c.comment));
346                    for r in &c.range {
347                        let range = shorten(&r);
348                        request.0.push(format!(
349                            "Range {}",
350                            range.as_ref().map(|x| x.as_str()).unwrap_or(r.as_str())
351                        ));
352                    }
353
354                    for d in &c.domain {
355                        let domain = shorten(&d);
356                        request.0.push(format!(
357                            "Domain {}",
358                            domain.as_ref().map(|x| x.as_str()).unwrap_or(d.as_str())
359                        ));
360                    }
361                }
362            }
363        }
364    }
365}