lang_turtle/
lib.rs

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        // pass
63        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
116/// Finds a resource
117/// if the location starts with http, it is a remote resource
118/// if the location fails to parse as a url, it is a file path
119///     anyway, try to read the file
120async 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            // This is the plan of approach: - add these ontologies to the predefined LOV things - need prefered prefix:
285            //          - filename.ttl -> filename
286            //          - parse and look for things?
287            //   - let the lov things also add prefixes to the prefix thing
288            //   - Profit
289
290            // spawn_or_insert(url, bundle, language_id, extra)
291            // we have the turtle files!
292            // we have 2 choices now:
293            //  add it to the ecs without links
294        };
295
296        client.spawn(fut);
297    }
298}