Skip to main content

swls_core/systems/
prefix_hover.rs

1//! Hover and goto-definition for prefix declarations and JSON-LD `@context`
2//! terms.
3//!
4//! A prefix line (`@prefix foaf: <…>`, `PREFIX foaf: <…>`) or a JSON-LD
5//! `@context` entry is never part of a triple, so the shared
6//! [`get_current_triple`] falls back to the *nearest* unrelated triple and the
7//! triple-based hover/goto systems then render information about the wrong term
8//! (or nothing). [`get_current_prefix`] detects this case from the parsed model
9//! — whose prefix declarations, unlike the derived `Triples`, carry spans —
10//! records a [`PrefixComponent`], and removes the stale [`TripleComponent`] so
11//! those systems no-op. [`hover_prefix`] then shows the namespace mapping and
12//! [`goto_prefix`] jumps to the ontology document.
13
14use bevy_ecs::prelude::*;
15use rdf_parsers::model::NamedNode;
16use swls_lov::LocalPrefix;
17use tracing::instrument;
18
19use crate::{
20    feature::goto_definition::GotoDefinitionRequest,
21    lsp_types::{Location, Range, Url},
22    prelude::*,
23    util::offset_to_position,
24};
25
26/// Set when the cursor is on a prefix declaration (Turtle/SPARQL/TriG) or a
27/// JSON-LD `@context` term. Carries the resolved namespace so hover and
28/// goto-definition can describe / navigate to it instead of an unrelated triple.
29#[derive(Component, Debug, Clone)]
30pub struct PrefixComponent {
31    /// Prefix name (`foaf`), or the empty string for the default `:` prefix.
32    pub name: String,
33    /// Resolved namespace / target IRI, as text.
34    pub namespace: String,
35    /// Range of the whole declaration — used as the hover range.
36    pub range: Range,
37}
38
39impl PrefixComponent {
40    /// A "namespace" prefix (usable as `name:local`) ends in a gen-delim; a
41    /// JSON-LD term alias points at one specific IRI and does not. Goto uses this
42    /// to leave alias resolution (e.g. Components.js) to language-specific
43    /// systems while it handles the ontology-file jump for real namespaces.
44    pub fn is_namespace(&self) -> bool {
45        self.namespace.ends_with('/') || self.namespace.ends_with('#')
46    }
47}
48
49/// Resolve a model [`NamedNode`] to its absolute IRI (a JSON-LD compact IRI
50/// carries its context-computed expansion in `computed`).
51fn named_node_iri(nn: &NamedNode) -> Option<String> {
52    match nn {
53        NamedNode::Full(iri, _) => Some(iri.clone()),
54        NamedNode::Prefixed {
55            computed: Some(c), ..
56        } => Some(c.clone()),
57        _ => None,
58    }
59}
60
61/// Detect whether the cursor sits on a prefix declaration / JSON-LD `@context`
62/// term and, if so, insert a [`PrefixComponent`] and drop the (necessarily
63/// wrong) [`TripleComponent`]. Shared by the Hover and GotoDefinition schedules.
64#[instrument(skip(query, commands))]
65pub fn get_current_prefix(
66    query: Query<
67        (Entity, &Element, &Prefixes, &PositionComponent, &RopeC),
68        Changed<PositionComponent>,
69    >,
70    mut commands: Commands,
71) {
72    for (e, element, prefixes, position, rope) in &query {
73        commands.entity(e).remove::<PrefixComponent>();
74
75        let Some(offset) = position_to_offset(position.0, &rope.0) else {
76            continue;
77        };
78
79        // Prefix declarations carry a span in the parsed model; the derived
80        // `Triples` do not, which is why we match against the model here.
81        let Some(decl) = element
82            .value()
83            .prefixes
84            .iter()
85            .filter(|p| p.span().contains(&offset))
86            .min_by_key(|p| p.span().end - p.span().start)
87        else {
88            continue;
89        };
90
91        let name = decl.value().prefix.value().clone();
92
93        // Prefer the derived (expanded, validated) namespace; fall back to the
94        // raw model value so a JSON-LD alias whose target does not parse as a URL
95        // still hovers.
96        let namespace = prefixes
97            .iter()
98            .find(|p| p.prefix == name)
99            .map(|p| p.url.to_string())
100            .or_else(|| named_node_iri(decl.value().value.value()))
101            .unwrap_or_default();
102
103        let (Some(start), Some(end)) = (
104            offset_to_position(decl.span().start, &rope.0),
105            offset_to_position(decl.span().end, &rope.0),
106        ) else {
107            continue;
108        };
109
110        commands
111            .entity(e)
112            .insert(PrefixComponent {
113                name,
114                namespace,
115                range: Range::new(start, end),
116            })
117            // A prefix line is never part of a triple: drop the nearest-triple
118            // fallback so the triple-based hover/goto systems skip this entity.
119            .remove::<TripleComponent>();
120    }
121}
122
123/// Hover system: describe the prefix → namespace mapping, enriched with the LOV
124/// ontology title when the namespace is known.
125pub fn hover_prefix(
126    mut query: Query<(&PrefixComponent, &mut HoverRequest)>,
127    lovs: Query<&LocalPrefix>,
128) {
129    for (prefix, mut req) in &mut query {
130        let heading = if prefix.name.is_empty() {
131            "Default prefix".to_string()
132        } else {
133            format!("Prefix `{}:`", prefix.name)
134        };
135
136        // LOV knows a human-readable title for many namespaces.
137        let title = lovs
138            .iter()
139            .find(|l| l.namespace.as_ref() == prefix.namespace && !l.title.is_empty())
140            .map(|l| l.title.as_ref());
141
142        let mut md = format!("{}\n\n<{}>", heading, prefix.namespace);
143        if let Some(title) = title {
144            md = format!("{}\n\n{}", md, title);
145        }
146
147        req.0.push(md);
148        if req.1.is_none() {
149            req.1 = Some(prefix.range.clone());
150        }
151    }
152}
153
154/// Goto-definition system: jump a real namespace prefix to its ontology
155/// document (the LOV-resolved file, or the namespace IRI itself). Term aliases
156/// (`is_namespace() == false`) are left to language-specific resolvers.
157#[instrument(skip(query, fs))]
158pub fn goto_prefix(
159    mut query: Query<(&PrefixComponent, &mut GotoDefinitionRequest)>,
160    fs: Res<Fs>,
161) {
162    for (prefix, mut req) in &mut query {
163        if !prefix.is_namespace() {
164            continue;
165        }
166        // Same resolution `derive_prefix_links` uses for the document-link, so
167        // goto lands on whatever file the server loads for this prefix.
168        let target = fs
169            .0
170            .lov_url(&prefix.namespace, &prefix.name)
171            .or_else(|| Url::parse(&prefix.namespace).ok());
172
173        if let Some(uri) = target {
174            req.0.push(Location {
175                uri,
176                range: Range::default(),
177            });
178        }
179    }
180}