Skip to main content

swls_lang_rdf_base/
rename.rs

1//! Shared, model-based rename for the text RDF syntaxes (Turtle / TriG / SPARQL /
2//! N3 — anything whose parsed [`Element`] is the [`Turtle`] model).
3//!
4//! Unlike the language-agnostic rename in `swls-core` (which slices the rope at a
5//! term span and classifies the raw string), these systems work off the parsed
6//! [`Turtle`] model.  Walking the model gives us:
7//!
8//! * exact term boundaries (no `<>`/`_:` string heuristics for *finding* the
9//!   token under the cursor), and
10//! * natural de-duplication — a subject written once with the `;` shorthand
11//!   appears exactly once in the model, so it yields exactly one edit.
12//!
13//! Each text RDF language opts in via [`setup_rename`].  JSON-LD deliberately
14//! stays on the core agnostic path because its concrete syntax wraps IRIs in
15//! quoted strings rather than `<>`.
16
17use std::ops::Range as StdRange;
18
19use bevy_ecs::prelude::*;
20use rdf_parsers::model::{BlankNode, NamedNode, Term, Turtle, PO};
21use swls_core::{
22    feature::rename::{PrepareRenameRequest, RenameEdits},
23    lsp_types::TextEdit,
24    prelude::*,
25    util::{offsets_to_range, position_to_offset},
26};
27
28use crate::traits::NamedNodeExt;
29
30/// Canonical identity of a renameable term.  Two occurrences are renamed
31/// together iff their keys are equal.
32#[derive(Clone, PartialEq, Eq)]
33enum RenameKey {
34    /// A (possibly prefixed) IRI, compared by its fully-expanded absolute form.
35    Iri(String),
36    /// A labelled blank node, compared by its label.
37    Blank(String),
38    /// A SPARQL variable, compared by its name.
39    Var(String),
40}
41
42/// Compute the canonical key of a term, or `None` if it is not renameable
43/// (literals, `a`, anonymous blank nodes, collections, invalid terms).
44fn term_key(term: &Term, turtle: &Turtle) -> Option<RenameKey> {
45    match term {
46        Term::NamedNode(nn) => match nn {
47            NamedNode::A(_) | NamedNode::Invalid => None,
48            _ => nn.expand(turtle).map(RenameKey::Iri),
49        },
50        Term::BlankNode(BlankNode::Named(label, _)) => Some(RenameKey::Blank(label.clone())),
51        Term::Variable(v) => Some(RenameKey::Var(v.0.clone())),
52        _ => None,
53    }
54}
55
56/// The bare text shown to the user in the rename input box for a term.
57fn term_placeholder(term: &Term) -> Option<String> {
58    match term {
59        Term::NamedNode(NamedNode::Full(iri, _)) => Some(iri.clone()),
60        Term::NamedNode(NamedNode::Prefixed { prefix, value, .. }) => {
61            Some(format!("{}:{}", prefix, value))
62        }
63        Term::BlankNode(BlankNode::Named(label, _)) => Some(format!("_:{}", label)),
64        Term::Variable(v) => Some(format!("?{}", v.0)),
65        _ => None,
66    }
67}
68
69/// Visit every term in the model, recursing into collections and the
70/// predicate/object positions of anonymous blank nodes.
71fn walk_terms(turtle: &Turtle, mut f: impl FnMut(&Spanned<Term>)) {
72    fn walk(term: &Spanned<Term>, f: &mut impl FnMut(&Spanned<Term>)) {
73        f(term);
74        match term.value() {
75            Term::Collection(items) => {
76                for item in items {
77                    walk(item, f);
78                }
79            }
80            Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
81                for po in pos {
82                    walk_po(po, f);
83                }
84            }
85            _ => {}
86        }
87    }
88    fn walk_po(po: &Spanned<PO>, f: &mut impl FnMut(&Spanned<Term>)) {
89        walk(&po.value().predicate, f);
90        for object in &po.value().object {
91            walk(object, f);
92        }
93    }
94    for triple in &turtle.triples {
95        let triple = triple.value();
96        walk(&triple.subject, &mut f);
97        for po in &triple.po {
98            walk_po(po, &mut f);
99        }
100    }
101}
102
103/// Find the innermost (smallest-span) renameable term containing `offset`,
104/// returning its span, canonical key and placeholder.
105///
106/// Matching is done in priority order so the cursor reliably targets the term it
107/// sits on, even at token boundaries:
108///
109/// 1. a term that *strictly* contains `offset` (`start <= offset < end`);
110/// 2. a term that strictly contains `offset + 1` — this recovers the editor's
111///    cursor when `Backend::adjust_position` has decremented it onto the
112///    *end-boundary* of a preceding token (e.g. the space before an empty
113///    `<>`), which would otherwise wrongly select that preceding token;
114/// 3. as a last resort, a term whose span *ends* exactly at `offset`.
115fn find_renameable_at(
116    turtle: &Turtle,
117    offset: usize,
118) -> Option<(StdRange<usize>, RenameKey, String)> {
119    find_renameable_with(turtle, offset, false)
120        .or_else(|| find_renameable_with(turtle, offset + 1, false))
121        .or_else(|| find_renameable_with(turtle, offset, true))
122}
123
124/// Smallest-span renameable term containing `offset`.  When `inclusive` is
125/// `false` the span is treated as half-open `[start, end)`; when `true` the end
126/// boundary is allowed (`[start, end]`).
127fn find_renameable_with(
128    turtle: &Turtle,
129    offset: usize,
130    inclusive: bool,
131) -> Option<(StdRange<usize>, RenameKey, String)> {
132    let mut best: Option<(StdRange<usize>, RenameKey, String)> = None;
133    walk_terms(turtle, |term| {
134        let span = term.span();
135        let contains = span.start <= offset && (offset < span.end || (inclusive && offset == span.end));
136        if !contains {
137            return;
138        }
139        let value = term.value();
140        let (Some(key), Some(placeholder)) = (term_key(value, turtle), term_placeholder(value))
141        else {
142            return;
143        };
144        let width = span.end - span.start;
145        let is_better = best
146            .as_ref()
147            .map(|(bspan, _, _)| width < bspan.end - bspan.start)
148            .unwrap_or(true);
149        if is_better {
150            best = Some((span.clone(), key, placeholder));
151        }
152    });
153    best
154}
155
156/// Model-based `prepare_rename`: report the range and placeholder of the term
157/// under the cursor.
158pub fn prepare_rename(
159    query: Query<(Entity, &Element, &RopeC, &PositionComponent, &DynLang)>,
160    mut commands: Commands,
161) {
162    for (entity, element, rope, position, lang) in &query {
163        // Only the languages that rename via this model-based path (Turtle-family)
164        // opt in; the rest are handled by the core agnostic rename.
165        if !lang.0.model_based_rename() {
166            continue;
167        }
168        commands.entity(entity).remove::<PrepareRenameRequest>();
169
170        let Some(offset) = position_to_offset(position.0, &rope.0) else {
171            continue;
172        };
173        let Some((span, _key, placeholder)) = find_renameable_at(element.value(), offset) else {
174            continue;
175        };
176        let Some(range) = offsets_to_range(span.start, span.end, &rope.0) else {
177            continue;
178        };
179        commands
180            .entity(entity)
181            .insert(PrepareRenameRequest { range, placeholder });
182    }
183}
184
185/// Model-based `rename`: replace every occurrence of the term under the cursor
186/// (matched by canonical key) with the wrapped new text.
187pub fn rename(
188    mut query: Query<(
189        &Element,
190        &RopeC,
191        &Label,
192        &PositionComponent,
193        &DynLang,
194        &mut RenameEdits,
195    )>,
196) {
197    for (element, rope, label, position, lang, mut edits) in &mut query {
198        if !lang.0.model_based_rename() {
199            continue;
200        }
201        let Some(offset) = position_to_offset(position.0, &rope.0) else {
202            continue;
203        };
204        let turtle = element.value();
205        let Some((_span, key, _placeholder)) = find_renameable_at(turtle, offset) else {
206            continue;
207        };
208        let new_text = lang.0.rename_wrap(&edits.1);
209
210        let mut collected: Vec<TextEdit> = Vec::new();
211        walk_terms(turtle, |term| {
212            if term_key(term.value(), turtle).as_ref() == Some(&key) {
213                if let Some(range) = offsets_to_range(term.span().start, term.span().end, &rope.0) {
214                    collected.push(TextEdit {
215                        range,
216                        new_text: new_text.clone(),
217                    });
218                }
219            }
220        });
221        for edit in collected {
222            edits.0.push((label.0.clone(), edit));
223        }
224    }
225}
226
227// [`prepare_rename`] and [`rename`] are registered once (non-generic) by
228// `register_rdf_lang`.  A language opts into this model-based path simply by
229// making [`LangHelper::model_based_rename`] return `true`; that same flag makes
230// the core agnostic rename skip the document, so the two never both fire.