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}