Skip to main content

swls_core/systems/
prefix.rs

1use std::{collections::HashSet, ops::Deref};
2
3use bevy_ecs::prelude::*;
4use swls_lov::LocalPrefix;
5use crate::lsp_types::Range;
6use tracing::instrument;
7
8use crate::{
9    lsp_types::{CompletionItemKind, TextEdit},
10    prelude::*,
11    systems::PrefixEntry,
12};
13
14pub const PREFIX_CC: &'static str = include_str!("./prefix_cc.txt");
15
16/// One defined prefix, maps prefix name to URL.
17#[derive(Debug, Clone)]
18pub struct Prefix {
19    pub prefix: String,
20    pub url: crate::lsp_types::Url,
21}
22
23/// [`Component`] containing defined prefixes and base URL.
24#[derive(Component, Debug)]
25pub struct Prefixes(pub Vec<Prefix>, pub crate::lsp_types::Url);
26
27impl Deref for Prefixes {
28    type Target = Vec<Prefix>;
29
30    fn deref(&self) -> &Self::Target {
31        &self.0
32    }
33}
34
35impl Prefixes {
36    pub fn shorten(&self, value: &str) -> Option<String> {
37        let try_shorten = |prefix: &Prefix| {
38            let short = value.strip_prefix(prefix.url.as_str())?;
39            Some(format!("{}:{}", prefix.prefix, short))
40        };
41        self.0.iter().flat_map(try_shorten).next()
42    }
43}
44
45enum PrefixLike<'a> {
46    Prefix(&'a Prefix),
47    Local(&'a LocalPrefix),
48    Entry(&'a PrefixEntry),
49}
50impl PrefixLike<'_> {
51    fn as_completion(
52        &self,
53        lang: &DynLang,
54        range: Range,
55        extra: Option<Vec<TextEdit>>,
56    ) -> SimpleCompletion {
57        let (name, title, namespace) = match self {
58            PrefixLike::Prefix(prefix) => (prefix.prefix.as_str(), None, prefix.url.as_str()),
59            PrefixLike::Local(local_prefix) => (
60                local_prefix.name.as_ref(),
61                Some(local_prefix.title.as_ref()),
62                local_prefix.namespace.as_ref(),
63            ),
64            PrefixLike::Entry(prefix_entry) => (
65                prefix_entry.name.as_ref(),
66                None,
67                prefix_entry.namespace.as_ref(),
68            ),
69        };
70
71        let nt = format!("{}:$0", name);
72        let mut completion = SimpleCompletion::new(
73            CompletionItemKind::MODULE,
74            format!("{}", name),
75            crate::lsp_types::TextEdit {
76                new_text: lang.quote(&nt),
77                range,
78            },
79        )
80        .documentation(namespace)
81        // .filter_text(new_text)
82        .as_snippet();
83
84        if let Some(title) = title {
85            completion = completion.label_description(title);
86        }
87
88        if let Some(extra) = extra {
89            for e in extra {
90                completion = completion.text_edit(e);
91            }
92        }
93
94        completion
95    }
96
97    fn name(&self) -> &str {
98        match self {
99            PrefixLike::Prefix(prefix) => prefix.prefix.as_str(),
100            PrefixLike::Local(local_prefix) => local_prefix.name.as_ref(),
101            PrefixLike::Entry(prefix_entry) => prefix_entry.name.as_ref(),
102        }
103    }
104
105    fn namespace(&self) -> &str {
106        match self {
107            PrefixLike::Prefix(prefix) => prefix.url.as_str(),
108            PrefixLike::Local(local_prefix) => local_prefix.namespace.as_ref(),
109            PrefixLike::Entry(prefix_entry) => prefix_entry.namespace.as_ref(),
110        }
111    }
112}
113
114pub fn prefix_completion_helper<'a>(
115    word: &TokenComponent,
116    prefixes: &Prefixes,
117    completions: &mut Vec<SimpleCompletion>,
118    mut extra_edits: impl FnMut(&str, &str) -> Option<Vec<TextEdit>>,
119    lovs: impl Iterator<Item = &'a LocalPrefix>,
120    prefix_cc: impl Iterator<Item = &'a PrefixEntry>,
121    lang: &DynLang,
122) {
123    let mut suggested = HashSet::new();
124    let text = lang.unquote(&word.text);
125    tracing::debug!("completion helper {:?}", word);
126
127    completions.extend(
128        lovs.map(|x| PrefixLike::Local(&x))
129            .chain(prefix_cc.map(|x| PrefixLike::Entry(x)))
130            .chain(prefixes.iter().map(PrefixLike::Prefix))
131            .filter(|p| p.name().starts_with(text))
132            .filter(|p| p.namespace().ends_with('#') || p.namespace().ends_with('/'))
133            .flat_map(|lov| {
134                if suggested.contains(lov.name()) {
135                    return None;
136                }
137
138                let completion = lov.as_completion(
139                    lang,
140                    word.range.clone(),
141                    extra_edits(&lov.name(), &lov.namespace()),
142                );
143
144                suggested.insert(lov.name().to_string());
145                Some(completion)
146            }),
147    );
148
149    // completions.extend(
150    //     lovs.filter(|lov| lov.name.starts_with(text))
151    //         .flat_map(|lov| {
152    //             if suggested.contains(&lov.namespace) {
153    //                 tracing::info!("suggested contains it {:?}", lov);
154    //                 return None;
155    //             }
156    //
157    //             let completion = PrefixLike::Local(lov).as_completion(
158    //                 lang,
159    //                 word.range.clone(),
160    //                 extra_edits(&lov.name, &lov.namespace),
161    //             );
162    //
163    //             suggested.insert(&lov.namespace);
164    //             Some(completion)
165    //         }),
166    // );
167    //
168    // completions.extend(
169    //     prefix_cc
170    //         .filter(|pref| pref.name.starts_with(text))
171    //         // .filter(|pref| !defined.contains(pref.namespace.as_ref()))
172    //         // .filter(|lov| {
173    //         //     !config
174    //         //         .prefix_disabled
175    //         //         .iter()
176    //         //         .any(|x| lov.name.starts_with(x.as_str()))
177    //         // })
178    //         .flat_map(|lov| {
179    //             if suggested.contains(&lov.namespace) {
180    //                 return None;
181    //             }
182    //             let mut new_text = format!("{}:", lov.name);
183    //             let filter_text = new_text.clone();
184    //             if new_text != text {
185    //                 new_text += "$0";
186    //                 let extra_edit = extra_edits(&lov.name, &lov.namespace);
187    //                 let completion = SimpleCompletion::new(
188    //                     CompletionItemKind::MODULE,
189    //                     format!("{}", lov.name),
190    //                     crate::lsp_types::TextEdit {
191    //                         new_text: lang.quote(&new_text),
192    //                         range: word.range.clone(),
193    //                     },
194    //                 )
195    //                 .documentation(lov.namespace.as_ref())
196    //                 .filter_text(filter_text)
197    //                 .as_snippet();
198    //
199    //                 let completion = extra_edit
200    //                     .into_iter()
201    //                     .flatten()
202    //                     .fold(completion, |completion: SimpleCompletion, edit| {
203    //                         completion.text_edit(edit)
204    //                     });
205    //
206    //                 suggested.insert(&lov.namespace);
207    //                 Some(completion)
208    //             } else {
209    //                 tracing::info!("new_text is word.text");
210    //                 None
211    //             }
212    //         }),
213    // );
214}
215
216/// Generic prefix completion for the text RDF languages (Turtle / TriG / SPARQL).
217///
218/// Suggests prefixes from the local LOV data, the prefix.cc list, and the
219/// prefixes already declared in the document.  When the chosen prefix is not yet
220/// declared, the language's [`prefix_edits`](crate::lang::LangHelper::prefix_edits)
221/// supplies an additional edit that inserts the declaration; when it is already
222/// declared no such edit is added.
223///
224/// This is registered **once** (in the shared completion schedule) and dispatches
225/// per-document through [`DynLang`], so it does not need a language marker filter.
226/// Languages whose prefix model is incompatible (e.g. JSON-LD `@context`) opt out
227/// via [`handles_prefix_completion`](crate::lang::LangHelper::handles_prefix_completion).
228#[instrument(skip(query, lovs, prefix_cc, config))]
229pub fn rdf_lov_undefined_prefix_completion(
230    mut query: Query<(
231        &TokenComponent,
232        &Source,
233        &RopeC,
234        &Prefixes,
235        &mut CompletionRequest,
236        &DynLang,
237    )>,
238    lovs: Query<&LocalPrefix>,
239    prefix_cc: Query<&PrefixEntry>,
240    config: Res<ServerConfig>,
241) {
242    if config.config.local.is_disabled(Disabled::CompletionPrefix) {
243        return;
244    }
245    let fmt = config.config.local.prefix_format.unwrap_or_default();
246    for (word, source, rope, prefixes, mut req, lang) in &mut query {
247        if lang.handles_prefix_completion() {
248            continue;
249        }
250        prefix_completion_helper(
251            word,
252            prefixes,
253            &mut req.0,
254            |name, location| {
255                if prefixes.iter().any(|p| p.prefix == name) {
256                    None
257                } else {
258                    lang.prefix_edits(&source.0, &rope.0, name, location, fmt)
259                }
260            },
261            lovs.iter(),
262            prefix_cc.iter(),
263            lang,
264        );
265    }
266}