1#![doc(
2 html_logo_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.png",
3 html_favicon_url = "https://ajuvercr.github.io/semantic-web-lsp/assets/icons/favicon.ico"
4)]
5use std::{borrow::Cow, collections::HashMap};
6
7use bevy_ecs::{
8 component::Component,
9 event::EntityEvent,
10 observer::On,
11 resource::Resource,
12 system::{Commands, Res},
13 world::{CommandQueue, World},
14};
15use chumsky::prelude::Simple;
16use lang::{context::Context, model::Turtle, parser::parse_turtle, tokenizer::parse_tokens_str};
17use lov::LocalPrefix;
18use lsp_core::{
19 feature::diagnostics::publish_diagnostics,
20 lang::{Lang, LangHelper},
21 lsp_types::{SemanticTokenType, Url},
22 prelude::*,
23 CreateEvent,
24};
25
26pub mod ecs;
27pub mod lang;
28
29use crate::ecs::{setup_completion, setup_formatting, setup_parsing};
30
31#[derive(Component)]
32pub struct TurtleLang;
33
34#[derive(Debug)]
35pub struct TurtleHelper;
36impl LangHelper for TurtleHelper {
37 fn keyword(&self) -> &[&'static str] {
38 &["@prefix", "@base", "a"]
39 }
40}
41
42pub fn setup_world<C: Client + ClientSync + Resource + Clone>(world: &mut World) {
43 let mut semantic_token_dict = world.resource_mut::<SemanticTokensDict>();
44 TurtleLang::LEGEND_TYPES.iter().for_each(|lt| {
45 if !semantic_token_dict.contains_key(lt) {
46 let l = semantic_token_dict.0.len();
47 semantic_token_dict.insert(lt.clone(), l);
48 }
49 });
50
51 world.add_observer(|trigger: On<CreateEvent>, mut commands: Commands| {
52 let e = &trigger.event();
53 match &e.language_id {
54 Some(x) if x == "turtle" => {
55 commands
56 .entity(e.event_target())
57 .insert((TurtleLang, DynLang(Box::new(TurtleHelper))));
58 return;
59 }
60 _ => {}
61 }
62 if trigger.event().url.as_str().ends_with(".ttl") {
64 commands
65 .entity(e.event_target())
66 .insert((TurtleLang, DynLang(Box::new(TurtleHelper))));
67 return;
68 }
69 });
70
71 world.schedule_scope(lsp_core::feature::DiagnosticsLabel, |_, schedule| {
72 schedule.add_systems(publish_diagnostics::<TurtleLang>);
73 });
74
75 world.schedule_scope(lsp_core::Startup, |_, schedule| {
76 schedule.add_systems(extract_known_prefixes_from_config::<C>);
77 });
78
79 setup_parsing(world);
80 setup_completion(world);
81 setup_formatting(world);
82}
83
84impl Lang for TurtleLang {
85 type Token = Token;
86
87 type TokenError = Simple<char>;
88
89 type Element = crate::lang::model::Turtle;
90
91 type ElementError = Simple<Token>;
92
93 const LANG: &'static str = "turtle";
94
95 const TRIGGERS: &'static [&'static str] = &[":"];
96 const CODE_ACTION: bool = true;
97 const HOVER: bool = true;
98
99 const LEGEND_TYPES: &'static [lsp_core::lsp_types::SemanticTokenType] = &[
100 semantic_token::BOOLEAN,
101 semantic_token::LANG_TAG,
102 SemanticTokenType::COMMENT,
103 SemanticTokenType::ENUM_MEMBER,
104 SemanticTokenType::ENUM,
105 SemanticTokenType::KEYWORD,
106 SemanticTokenType::NAMESPACE,
107 SemanticTokenType::NUMBER,
108 SemanticTokenType::PROPERTY,
109 SemanticTokenType::STRING,
110 SemanticTokenType::VARIABLE,
111 ];
112
113 const PATTERN: Option<&'static str> = None;
114}
115
116async fn find(location: &str, fs: &Fs, client: &impl Client) -> Option<Vec<(String, Url)>> {
121 if location.starts_with("http") {
122 let url = Url::parse(location).ok()?;
123 let content = client.fetch(&location, &HashMap::new()).await.ok()?.body;
124 Some(vec![(content, url)])
125 } else {
126 if let Ok(url) = Url::parse(&location) {
127 let content = fs.0.read_file(&url).await?;
128 Some(vec![(content, url)])
129 } else {
130 let files = fs.0.glob_read(&location).await?;
131 Some(
132 files
133 .into_iter()
134 .flat_map(|File { content, name }| {
135 if let Some(url) = file_name_to_url(&name) {
136 Some((content, url))
137 } else {
138 None
139 }
140 })
141 .collect(),
142 )
143 }
144 }
145}
146#[cfg(not(target_arch = "wasm32"))]
147fn file_name_to_url(name: &str) -> Option<Url> {
148 lsp_core::lsp_types::Url::from_file_path(name).ok()
149}
150
151#[cfg(target_arch = "wasm32")]
152fn file_name_to_url(name: &str) -> Option<Url> {
153 None
154}
155
156fn prefix_from_url(url: &Url) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
157 let segments = url.path_segments()?;
158 let last = segments.last()?;
159
160 let prefix = last.rsplit_once('.').map(|(x, _)| x).unwrap_or(last);
161 let prefix: Cow<'static, str> = Cow::Owned(prefix.to_string());
162
163 let url = Cow::Owned(url.to_string());
164
165 Some((prefix, url))
166}
167
168fn prefix_from_declaration(
169 turtle: &Triples2<'_>,
170) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
171 let subject = turtle
172 .iter()
173 .find(|t| {
174 t.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
175 && t.object.as_str() == "http://www.w3.org/2002/07/owl#Ontology"
176 })?
177 .subject
178 .to_owned();
179
180 let prefix = turtle
181 .iter()
182 .find(|t| {
183 t.subject.eq(&subject)
184 && t.predicate.as_str() == "http://purl.org/vocab/vann/preferredNamespacePrefix"
185 })?
186 .object
187 .as_str();
188 let prefix = Cow::Owned(String::from(prefix));
189
190 let url = turtle
191 .iter()
192 .find(|t| {
193 t.subject.eq(&subject)
194 && t.predicate.as_str() == "http://purl.org/vocab/vann/preferredNamespaceUri"
195 })?
196 .object
197 .as_str();
198 let url = Cow::Owned(String::from(url));
199
200 Some((prefix, url))
201}
202
203fn prefix_from_prefixes(
204 model: &Turtle,
205 turtle: &Triples2<'_>,
206) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
207 let shortened: Vec<_> = turtle
208 .iter()
209 .filter(|x| x.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
210 .flat_map(|t| model.shorten(t.subject.as_str()))
211 .collect();
212
213 let mut counts = HashMap::new();
214 for short in &shortened {
215 if let Some((prefix, _)) = short.split_once(':') {
216 let key: &mut usize = counts.entry(prefix).or_default();
217 *key += 1;
218 }
219 }
220
221 let (prefix, _) = counts.into_iter().max_by_key(|(_, e)| *e)?;
222
223 let prefix = model
224 .prefixes
225 .iter()
226 .find(|p| p.prefix.as_str() == prefix)?;
227
228 Some((
229 Cow::Owned(String::from(prefix.prefix.as_str())),
230 Cow::Owned(prefix.value.0.expand(model)?),
231 ))
232}
233
234fn prefix_from_source(url: &Url, source: &str) -> Option<(Cow<'static, str>, Cow<'static, str>)> {
235 let (tok, _) = parse_tokens_str(source);
236 let empty = Context::new();
237 let (turtle, _) = parse_turtle(&url, tok, source.len(), empty.ctx());
238
239 let triples = turtle.get_simple_triples().ok()?;
240
241 prefix_from_declaration(&triples).or_else(|| prefix_from_prefixes(&turtle, &triples))
242}
243
244pub fn extract_known_prefixes_from_config<C: Client + ClientSync + Resource + Clone>(
245 config: Res<ServerConfig>,
246 client: Res<C>,
247 fs: Res<Fs>,
248 sender: Res<CommandSender>,
249) {
250 for on in config.config.local.ontologies.iter().cloned() {
251 let c = client.clone();
252 let fs = fs.clone();
253
254 let sender = sender.0.clone();
255
256 let fut = async move {
257 let Some(files) = find(&on, &fs, &c).await else {
258 return;
259 };
260
261 let mut queue = CommandQueue::default();
262 for (content, url) in files {
263 let Some((prefix, url)) =
264 prefix_from_source(&url, &content).or_else(|| prefix_from_url(&url))
265 else {
266 continue;
267 };
268
269 let lov = LocalPrefix {
270 location: Cow::Owned(url.to_string()),
271 content: Cow::Owned(content),
272 name: prefix.clone(),
273 title: prefix,
274 rank: 0,
275 };
276
277 queue.push(move |world: &mut World| {
278 world.spawn(lov);
279 });
280 }
281
282 let _ = sender.unbounded_send(queue);
283
284 };
295
296 client.spawn(fut);
297 }
298}