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}