Skip to main content

swls_core/systems/
prefix.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ops::Deref,
4};
5
6use crate::{
7    lsp_types::{CompletionItemKind, Diagnostic, DiagnosticSeverity, TextDocumentItem, TextEdit},
8    systems::PrefixEntry,
9};
10use bevy_ecs::prelude::*;
11use swls_lov::LocalPrefix;
12use tower_lsp::lsp_types::DiagnosticTag;
13use tracing::{debug, instrument};
14
15use crate::prelude::*;
16
17pub const PREFIX_CC: &'static str = include_str!("./prefix_cc.txt");
18
19/// One defined prefix, maps prefix to url
20#[derive(Debug, Clone)]
21pub struct Prefix {
22    pub prefix: String,
23    pub url: crate::lsp_types::Url,
24}
25
26/// [`Component`] that containing defined prefixes and base URL.
27///
28/// [`swls_core`](crate) uses [`Prefixes`] in different systems, for example
29/// - to check for undefined prefixes diagnostics with
30/// [`undefined_prefix`](crate::prelude::systems::undefined_prefix)
31/// - derive linked documents [`DocumentLinks`] with
32/// [`derive_prefix_links`](crate::prelude::systems::derive_prefix_links)
33#[derive(Component, Debug)]
34pub struct Prefixes(pub Vec<Prefix>, pub crate::lsp_types::Url);
35impl Deref for Prefixes {
36    type Target = Vec<Prefix>;
37
38    fn deref(&self) -> &Self::Target {
39        &self.0
40    }
41}
42impl Prefixes {
43    pub fn shorten(&self, value: &str) -> Option<String> {
44        let try_shorten = |prefix: &Prefix| {
45            let short = value.strip_prefix(prefix.url.as_str())?;
46            Some(format!("{}:{}", prefix.prefix, short))
47        };
48
49        self.0.iter().flat_map(try_shorten).next()
50    }
51
52    pub fn expand(&self, token: &Token) -> Option<String> {
53        match token {
54            Token::PNameLN(pref, x) => {
55                let pref = pref.as_ref().map(|x| x.as_str()).unwrap_or("");
56                let prefix = self.0.iter().find(|x| &x.prefix == pref)?;
57                Some(format!("{}{}", prefix.url, x))
58            }
59            Token::IRIRef(x) => {
60                return self.1.join(&x).ok().map(|x| x.to_string());
61            }
62            _ => None,
63        }
64    }
65
66    pub fn expand_json(&self, token: &Token) -> Option<String> {
67        match token {
68            Token::Str(pref, _) => {
69                if let Some(x) = pref.find(':') {
70                    let prefix = &pref[..x];
71                    if let Some(exp) = self.0.iter().find(|x| &x.prefix == prefix) {
72                        return Some(format!("{}{}", exp.url.as_str(), &pref[x + 1..]));
73                    }
74                } else {
75                    if let Some(exp) = self.0.iter().find(|x| &x.prefix == pref) {
76                        return Some(exp.url.as_str().to_string());
77                    }
78                }
79
80                return Some(
81                    self.1
82                        .join(&pref)
83                        .ok()
84                        .map(|x| x.to_string())
85                        .unwrap_or(pref.to_string()),
86                );
87            }
88            _ => None,
89        }
90    }
91}
92
93pub fn prefix_completion_helper<'a>(
94    word: &TokenComponent,
95    prefixes: &Prefixes,
96    completions: &mut Vec<SimpleCompletion>,
97    mut extra_edits: impl FnMut(&str, &str) -> Option<Vec<TextEdit>>,
98    lovs: impl Iterator<Item = &'a LocalPrefix>,
99    prefix_cc: impl Iterator<Item = &'a PrefixEntry>,
100    config: &LocalConfig,
101    // known: &KnownPrefixes,
102) {
103    match word.token.value() {
104        Token::Invalid(_) => {}
105        _ => return,
106    }
107
108    let mut defined = HashSet::new();
109    for p in prefixes.0.iter() {
110        defined.insert(p.url.as_str());
111    }
112
113    let mut suggested = HashSet::new();
114    completions.extend(
115        lovs.filter(|lov| lov.name.starts_with(&word.text))
116            .filter(|lov| !defined.contains(lov.namespace.as_ref()))
117            .flat_map(|lov| {
118                if suggested.contains(&lov.namespace) {
119                    return None;
120                }
121                let new_text = format!("{}:", lov.name);
122                // let sort_text = format!("{}", lov.rank);
123                let filter_text = new_text.clone();
124                if new_text != word.text {
125                    let extra_edit = extra_edits(&lov.name, &lov.namespace)?;
126                    let completion = SimpleCompletion::new(
127                        CompletionItemKind::MODULE,
128                        format!("{}", lov.name),
129                        crate::lsp_types::TextEdit {
130                            new_text,
131                            range: word.range.clone(),
132                        },
133                    )
134                    .label_description(lov.title.as_ref())
135                    .documentation(lov.namespace.as_ref())
136                    // .sort_text(sort_text)
137                    .filter_text(filter_text);
138
139                    let completion = extra_edit
140                        .into_iter()
141                        .fold(completion, |completion: SimpleCompletion, edit| {
142                            completion.text_edit(edit)
143                        });
144                    suggested.insert(&lov.namespace);
145                    Some(completion)
146                } else {
147                    None
148                }
149            }),
150    );
151    completions.extend(
152        prefix_cc
153            .filter(|pref| pref.name.starts_with(&word.text))
154            .filter(|pref| !defined.contains(pref.namespace.as_ref()))
155            .filter(|lov| {
156                !config
157                    .prefix_disabled
158                    .iter()
159                    .any(|x| lov.name.starts_with(x.as_str()))
160            })
161            .flat_map(|lov| {
162                if suggested.contains(&lov.namespace) {
163                    return None;
164                }
165                let new_text = format!("{}:", lov.name);
166                // let sort_text = format!("{}", lov.rank);
167                let filter_text = new_text.clone();
168                if new_text != word.text {
169                    let extra_edit = extra_edits(&lov.name, &lov.namespace)?;
170                    let completion = SimpleCompletion::new(
171                        CompletionItemKind::MODULE,
172                        format!("{}", lov.name),
173                        crate::lsp_types::TextEdit {
174                            new_text,
175                            range: word.range.clone(),
176                        },
177                    )
178                    .documentation(lov.namespace.as_ref())
179                    // .sort_text(sort_text)
180                    .filter_text(filter_text);
181
182                    let completion = extra_edit
183                        .into_iter()
184                        .fold(completion, |completion: SimpleCompletion, edit| {
185                            completion.text_edit(edit)
186                        });
187                    suggested.insert(&lov.namespace);
188                    Some(completion)
189                } else {
190                    None
191                }
192            }),
193    );
194}
195
196pub fn undefined_prefix(
197    query: Query<
198        (&Tokens, &Prefixes, &Wrapped<TextDocumentItem>, &RopeC),
199        (Or<(Changed<Prefixes>, Changed<Tokens>)>, With<Open>),
200    >,
201    mut client: ResMut<DiagnosticPublisher>,
202) {
203    for (tokens, prefixes, item, rope) in &query {
204        let mut diagnostics: Vec<Diagnostic> = Vec::new();
205        for t in &tokens.0 {
206            match t.value() {
207                Token::PNameLN(x, _) => {
208                    let pref = x.as_ref().map(|x| x.as_str()).unwrap_or("");
209                    let found = prefixes.0.iter().find(|x| x.prefix == pref).is_some();
210                    if !found {
211                        if let Some(range) = range_to_range(t.span(), &rope) {
212                            diagnostics.push(Diagnostic {
213                                range,
214                                severity: Some(DiagnosticSeverity::ERROR),
215                                source: Some(String::from("SWLS")),
216                                message: format!("Undefined prefix {}", pref),
217                                related_information: None,
218                                ..Default::default()
219                            })
220                        }
221                    }
222                }
223                _ => {}
224            }
225        }
226        let _ = client.publish(&item.0, diagnostics, "undefined_prefix");
227    }
228}
229
230/// Diagnostic system that warns about prefix declarations that are never used.
231///
232/// For example, `@prefix foaf: <http://xmlns.com/foaf/0.1/> .` without any `foaf:` usage
233/// in the document produces a `Warning` diagnostic.
234pub fn unused_prefix(
235    query: Query<
236        (&Tokens, &Prefixes, &Wrapped<TextDocumentItem>, &RopeC),
237        (Or<(Changed<Prefixes>, Changed<Tokens>)>, With<Open>),
238    >,
239    mut client: ResMut<DiagnosticPublisher>,
240) {
241    for (tokens, prefixes, item, rope) in &query {
242        let mut diagnostics: Vec<Diagnostic> = Vec::new();
243
244        // Collect which prefixes are used (non-declaration PNameLN tokens)
245        // and where each prefix is declared (the PNameLN span right after @prefix/PREFIX).
246        let mut used_prefixes: HashSet<&str> = HashSet::new();
247        let mut declaration_spans: HashMap<&str, std::ops::Range<usize>> = HashMap::new();
248
249        for (i, t) in tokens.0.iter().enumerate() {
250            if let Token::PNameLN(Some(pref), _) = t.value() {
251                // A prefix name token that is immediately preceded (ignoring comments)
252                // by a PrefixTag or SparqlPrefix is a declaration, not a use.
253                let is_declaration = tokens.0[..i]
254                    .iter()
255                    .rev()
256                    .find(|tok| !matches!(tok.value(), Token::Comment(_)))
257                    .map(|tok| matches!(tok.value(), Token::PrefixTag | Token::SparqlPrefix))
258                    .unwrap_or(false);
259
260                if is_declaration {
261                    declaration_spans.insert(pref.as_str(), t.span().clone());
262                } else {
263                    used_prefixes.insert(pref.as_str());
264                }
265            }
266        }
267
268        // Any declared prefix that was not used gets a warning
269        for prefix in prefixes.0.iter() {
270            if !used_prefixes.contains(prefix.prefix.as_str()) {
271                if let Some(span) = declaration_spans.get(prefix.prefix.as_str()) {
272                    if let Some(range) = range_to_range(span, rope) {
273                        diagnostics.push(Diagnostic {
274                            range,
275                            tags: Some(vec![DiagnosticTag::UNNECESSARY]),
276                            severity: Some(DiagnosticSeverity::INFORMATION),
277                            source: Some(String::from("SWLS")),
278                            message: format!(
279                                "Prefix '{}' is declared but never used",
280                                prefix.prefix
281                            ),
282                            related_information: None,
283                            ..Default::default()
284                        });
285                    }
286                }
287            }
288        }
289
290        let _ = client.publish(&item.0, diagnostics, "unused_prefix");
291    }
292}
293
294#[instrument(skip(query))]
295pub fn defined_prefix_completion(
296    mut query: Query<(&TokenComponent, &Prefixes, &mut CompletionRequest)>,
297) {
298    for (word, prefixes, mut req) in &mut query {
299        let st = &word.text;
300        let pref = if let Some(idx) = st.find(':') {
301            &st[..idx]
302        } else {
303            &st
304        };
305
306        debug!("matching {}", pref);
307
308        let completions = prefixes
309            .0
310            .iter()
311            .filter(|p| p.prefix.as_str().starts_with(pref))
312            .flat_map(|x| {
313                let new_text = format!("{}:", x.prefix.as_str());
314                if new_text != word.text {
315                    Some(
316                        SimpleCompletion::new(
317                            CompletionItemKind::MODULE,
318                            format!("{}", x.prefix.as_str()),
319                            crate::lsp_types::TextEdit {
320                                new_text,
321                                range: word.range.clone(),
322                            },
323                        )
324                        .documentation(x.url.as_str()),
325                    )
326                } else {
327                    None
328                }
329            });
330
331        req.0.extend(completions);
332    }
333}