Skip to main content

swls_lang_turtle/ecs/
mod.rs

1use bevy_ecs::{prelude::*, world::World};
2use completion::{infer_predicate_position_from_cst, subject_completion};
3use format::format_turtle_system;
4use swls_core::prelude::*;
5
6use crate::TurtleLang;
7
8mod code_action;
9mod completion;
10mod format;
11pub mod parse;
12
13pub fn setup_parsing(world: &mut World) {
14    use swls_core::feature::parse::*;
15    world.schedule_scope(ParseLabel, |_, schedule| {
16        schedule.add_systems((
17            parse::parse_turtle_system,
18            swls_lang_rdf_base::parse::derive_prefixes_system::<TurtleLang>
19                .after(parse::parse_turtle_system)
20                .before(prefixes),
21            parse::derive_triples_system::<TurtleLang>
22                .after(parse::parse_turtle_system)
23                .before(triples),
24        ));
25    });
26}
27
28pub fn setup_formatting(world: &mut World) {
29    world.schedule_scope(FormatLabel, |_, schedule| {
30        schedule.add_systems(format_turtle_system);
31    });
32}
33
34pub fn setup_code_action(world: &mut World) {
35    use swls_core::feature::code_action::Label as CodeActionLabel;
36    world.schedule_scope(CodeActionLabel, |_, schedule| {
37        schedule.add_systems(code_action::organize_imports);
38    });
39}
40
41pub fn setup_completion(world: &mut World) {
42    use swls_core::feature::completion::*;
43    world.schedule_scope(CompletionLabel, |_, schedule| {
44        schedule.add_systems((
45            subject_completion.after(generate_completions),
46            infer_predicate_position_from_cst.after(generate_completions),
47        ));
48    });
49}
50
51#[cfg(test)]
52mod tests {
53    use futures::executor::block_on;
54    use swls_core::text::LineIndex;
55    use swls_core::{
56        components::*,
57        prelude::{diagnostics::DiagnosticItem, *},
58    };
59    use swls_test_utils::{create_file, setup_world, TestClient};
60    use test_log::test;
61
62    #[test]
63    fn diagnostics_work() {
64        let (mut world, mut rx) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
65
66        // t2: prefix declared AND used (foaf:foaf) — but foaf:foaf misses predicate/object → syntax error
67        let t2 = "\n@prefix foaf: <>.\nfoaf:foaf\n            ";
68        // t3: prefix declared but not used (foa is an invalid token, not a prefixed name)
69        let t3 = "\n@prefix foaf: <>.\nfoa\n            ";
70
71        let mut last_diags = move || -> Vec<swls_core::lsp_types::Diagnostic> {
72            let mut items: Vec<DiagnosticItem> = Vec::new();
73            while let Ok(Some(x)) = rx.try_next() {
74                items.push(x);
75            }
76            items
77                .into_iter()
78                .last()
79                .map(|i| i.diagnostics)
80                .unwrap_or_default()
81        };
82
83        let entity = create_file(&mut world, t2, "http://example.com/ns#", "turtle", Open);
84        world.run_schedule(ParseLabel);
85
86        // t2: foaf IS used (foaf:foaf is a subject), but it's missing predicate+object → syntax errors
87        let diags = last_diags();
88        assert!(
89            !diags.is_empty(),
90            "t2: expected syntax errors for incomplete triple"
91        );
92
93        // t3: 'foa' is an invalid token → syntax errors
94        world
95            .entity_mut(entity)
96            .insert((Source(t3.to_string()), RopeC(LineIndex::new(t3))));
97        world.run_schedule(ParseLabel);
98
99        let diags = last_diags();
100        assert!(
101            !diags.is_empty(),
102            "t3: expected syntax errors for invalid token"
103        );
104    }
105
106    #[test_log::test]
107    fn fetch_lov_properties_test() {
108        let mut client = TestClient::new();
109        client.add_res("http://xmlns.com/foaf/0.1/", " @prefix foaf: <>. ");
110        let (mut world, _) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
111
112        let t1 = " @prefix foaf: <http://xmlns.com/foaf/0.1/>.";
113        create_file(&mut world, t1, "http://example.com/ns#", "turtle", Open);
114
115        let c = world.resource::<TestClient>().clone();
116        block_on(c.await_futures(|| world.run_schedule(swls_core::feature::ParseLabel)));
117
118        // We added 3 ontologies that are always present
119        assert!(world.entities().len() >= 2 + 3);
120    }
121
122    #[test]
123    fn turtle_does_prefix_links() {
124        let (mut world, _) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
125
126        let t1 = " @prefix foaf: <http://xmlns.com/foaf/0.1/>.";
127        let entity = create_file(&mut world, t1, "http://example.com/ns#", "turtle", Open);
128
129        let links: &DocumentLinks = world.entity(entity).get().expect("document links exists");
130        assert_eq!(links.len(), 1);
131        assert_eq!(links[0].0.as_str(), "file:///tmp/swls/test/foaf.ttl");
132        assert_eq!(links[0].1, "prefix import");
133    }
134}
135
136/// Prefix-usage diagnostics must see prefixes that appear only in a literal's
137/// datatype (`"5"^^xsd:integer`).  These exercise the model-based
138/// `prefix_diagnostic_helper` directly, since the derived triples store datatypes
139/// pre-expanded and cannot express the prefix.
140#[cfg(test)]
141mod prefix_datatype_tests {
142    use swls_core::lsp_types::Url;
143    use swls_core::prelude::*;
144    use swls_core::systems::prefix_diagnostic_helper;
145    use swls_core::text::LineIndex;
146
147    use crate::lang::parser::parse_new;
148
149    const BASE: &str = "http://example.com/";
150
151    fn run(src: &str, declared: &[&str], report_unused: bool) -> Vec<String> {
152        let (turtle, ..) = parse_new(src, BASE, None);
153        let prefixes = Prefixes(
154            declared
155                .iter()
156                .map(|p| Prefix {
157                    prefix: p.to_string(),
158                    url: Url::parse("http://www.w3.org/2001/XMLSchema#").unwrap(),
159                })
160                .collect(),
161            Url::parse(BASE).unwrap(),
162        );
163        let rope = RopeC(LineIndex::new(src));
164        let label = Label(Url::parse(BASE).unwrap());
165
166        let (diags, _) = prefix_diagnostic_helper(
167            &turtle,
168            &prefixes,
169            &rope,
170            &label,
171            std::iter::empty(),
172            std::iter::empty(),
173            |_, _| Vec::new(),
174            true,
175            report_unused,
176        );
177        diags.into_iter().map(|d| d.message).collect()
178    }
179
180    #[test]
181    fn datatype_prefix_counts_as_used() {
182        let src = "@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\n<s> <p> \"5\"^^xsd:integer .\n";
183        let msgs = run(src, &["xsd"], true);
184        assert!(
185            !msgs.iter().any(|m| m.contains("never used")),
186            "xsd used only in a datatype must not be reported unused: {msgs:?}",
187        );
188    }
189
190    #[test]
191    fn undefined_datatype_prefix_is_flagged() {
192        let src = "<s> <p> \"5\"^^xsd:integer .\n";
193        let msgs = run(src, &[], false);
194        assert!(
195            msgs.iter().any(|m| m.contains("Undefined prefix \"xsd\"")),
196            "xsd in a datatype but undeclared must be flagged: {msgs:?}",
197        );
198    }
199}