Skip to main content

swls_core/feature/
rename.rs

1use bevy_ecs::{
2    prelude::*,
3    schedule::{IntoScheduleConfigs, ScheduleLabel},
4};
5use sophia_api::quad::Quad as _;
6use tracing::instrument;
7
8pub use crate::util::triple::get_current_triple;
9use crate::{lsp_types::TextEdit, prelude::*};
10
11/// [`Component`] indicating that the current document is handling a PrepareRename request.
12#[derive(Component, Debug)]
13pub struct PrepareRenameRequest {
14    pub range: crate::lsp_types::Range,
15    pub placeholder: String,
16}
17
18/// [`ScheduleLabel`] related to the PrepareRename schedule
19#[derive(ScheduleLabel, Clone, Eq, PartialEq, Debug, Hash)]
20pub struct PrepareRename;
21
22/// [`Component`] for collecting rename [`TextEdit`]s.
23#[derive(Component, Debug)]
24pub struct RenameEdits(
25    pub Vec<(crate::lsp_types::Url, crate::lsp_types::TextEdit)>,
26    pub String,
27);
28
29/// [`ScheduleLabel`] related to the Rename schedule
30#[derive(ScheduleLabel, Clone, Eq, PartialEq, Debug, Hash)]
31pub struct Rename;
32
33pub fn setup_schedules(world: &mut World) {
34    let mut prepare_rename_schedule = Schedule::new(PrepareRename);
35    prepare_rename_schedule
36        .add_systems((get_current_triple, prepare_rename.after(get_current_triple)));
37    world.add_schedule(prepare_rename_schedule);
38
39    let mut rename_schedule = Schedule::new(Rename);
40    rename_schedule.add_systems((get_current_triple, rename.after(get_current_triple)));
41    world.add_schedule(rename_schedule);
42}
43
44#[instrument(skip(query, commands))]
45pub fn prepare_rename(
46    query: Query<(Entity, &RopeC, &DynLang, Option<&TripleComponent>)>,
47    mut commands: Commands,
48) {
49    for (e, rope, lang, m_triple) in &query {
50        // Text RDF syntaxes rename via the model-based systems; skip them here
51        // (don't even clear the request — the model system owns it).
52        if lang.0.model_based_rename() {
53            continue;
54        }
55        commands.entity(e).remove::<PrepareRenameRequest>();
56        if let Some(triple) = m_triple {
57            use sophia_api::term::TermKind;
58            let renameable = matches!(
59                triple.kind(),
60                TermKind::Iri | TermKind::BlankNode | TermKind::Variable
61            );
62            if renameable {
63                let span = match triple.target {
64                    TripleTarget::Subject => &triple.triple.subject.span,
65                    TripleTarget::Predicate => &triple.triple.predicate.span,
66                    TripleTarget::Object => &triple.triple.object.span,
67                    TripleTarget::Graph => continue,
68                };
69
70                let Some(raw) = rope.0.byte_slice(span.start..span.end) else {
71                    continue;
72                };
73                let raw = raw.to_string();
74                let inner = lang.0.rename_placeholder(&raw);
75
76                // Guard: inner must be non-empty
77                if inner.is_empty() {
78                    continue;
79                }
80
81                // `inner` is a sub-slice of `raw`; work entirely in *byte* offsets
82                // so the math stays consistent with `span` (also bytes) and with
83                // `range_to_range`. Mixing in char counts here previously produced
84                // wrong ranges after multi-byte characters.
85                let prefix_offset = inner.as_ptr() as usize - raw.as_ptr() as usize;
86                let inner_start = span.start + prefix_offset;
87                let inner_end = inner_start + inner.len();
88
89                if inner_start >= inner_end || inner_end > span.end {
90                    continue;
91                }
92
93                if let Some(range) = range_to_range(&(inner_start..inner_end), &rope.0) {
94                    let placeholder = inner.to_string();
95                    commands
96                        .entity(e)
97                        .insert(PrepareRenameRequest { range, placeholder });
98                    continue;
99                }
100            }
101        }
102        tracing::debug!("Didn't find a renameable triple");
103    }
104}
105
106#[instrument(skip(query))]
107pub fn rename(mut query: Query<(&TripleComponent, &Triples, &RopeC, &Label, &DynLang, &mut RenameEdits)>) {
108    for (triple, triples, rope, label, lang, mut edits) in &mut query {
109        // Text RDF syntaxes rename via the model-based systems; skip them here.
110        if lang.0.model_based_rename() {
111            continue;
112        }
113        let Some(target) = triple.term() else {
114            continue;
115        };
116        let new_text = lang.0.rename_wrap(&edits.1);
117
118        // Collect unique byte-span ranges to avoid duplicate edits when the same
119        // term appears as subject/predicate/object across multiple triples.
120        let mut seen_spans: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
121
122        for quad in triples.0.iter() {
123            for term in [quad.s(), quad.p(), quad.o()] {
124                if term == target {
125                    let key = (term.span.start, term.span.end);
126                    if seen_spans.insert(key) {
127                        if let Some(range) = range_to_range(&term.span, &rope.0) {
128                            edits.0.push((
129                                label.0.clone(),
130                                TextEdit {
131                                    range,
132                                    new_text: new_text.clone(),
133                                },
134                            ));
135                        }
136                    }
137                }
138            }
139        }
140    }
141}