1use std::collections::HashMap;
11use std::ops::Range as StdRange;
12
13use bevy_ecs::prelude::*;
14use rdf_parsers::model::{BlankNode, Term, Turtle, PO};
15use swls_core::{
16 feature::code_action::CodeActionRequest,
17 lsp_types::{CodeAction, CodeActionKind, Range, TextEdit, WorkspaceEdit},
18 prelude::*,
19 util::{offset_to_position, offsets_to_range, position_to_offset},
20};
21
22fn find_unnamed_in_term(
25 term: &Spanned<Term>,
26 offset: usize,
27 best: &mut Option<(StdRange<usize>, bool)>,
28) {
29 let span = term.span();
30 if offset < span.start || offset > span.end {
31 return;
32 }
33 match term.value() {
34 Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
35 *best = Some((span.clone(), !pos.is_empty()));
37 for po in pos {
38 find_unnamed_in_po(po, offset, best);
39 }
40 }
41 Term::Collection(items) => {
42 for item in items {
43 find_unnamed_in_term(item, offset, best);
44 }
45 }
46 _ => {}
47 }
48}
49
50fn find_unnamed_in_po(po: &Spanned<PO>, offset: usize, best: &mut Option<(StdRange<usize>, bool)>) {
51 let po = po.value();
52 find_unnamed_in_term(&po.predicate, offset, best);
53 for object in &po.object {
54 find_unnamed_in_term(object, offset, best);
55 }
56}
57
58fn fresh_blank_label(source: &str) -> String {
60 let mut n = 0;
61 loop {
62 let candidate = format!("_:b{}", n);
63 if !source.contains(&candidate) {
64 return candidate;
65 }
66 n += 1;
67 }
68}
69
70pub fn extract_blank_node(
88 mut query: Query<(
89 &Element,
90 &Source,
91 &RopeC,
92 &Label,
93 &PositionComponent,
94 &DynLang,
95 &mut CodeActionRequest,
96 )>,
97 config: Res<ServerConfig>,
98) {
99 if config.config.local.is_disabled(Disabled::CodeActionBlankNodeRefactor) {
100 return;
101 }
102 for (element, source, rope, label, position, lang, mut req) in &mut query {
103 if !lang.0.blank_node_code_actions() {
104 continue;
105 }
106 let Some(offset) = position_to_offset(position.0, &rope.0) else {
107 continue;
108 };
109 let turtle: &Turtle = element.value();
110
111 let mut best: Option<(StdRange<usize>, bool)> = None;
112 for triple in &turtle.triples {
113 let triple = triple.value();
114 find_unnamed_in_term(&triple.subject, offset, &mut best);
115 for po in &triple.po {
116 find_unnamed_in_po(po, offset, &mut best);
117 }
118 }
119
120 let Some((bspan, has_content)) = best else {
121 continue;
122 };
123 if !has_content {
125 continue;
126 }
127
128 let Some(raw) = source.0.get(bspan.start..bspan.end) else {
130 continue;
131 };
132 let inner = raw.trim();
133 let inner = inner.strip_prefix('[').unwrap_or(inner);
134 let inner = inner.strip_suffix(']').unwrap_or(inner);
135 let inner = inner.trim();
136 if inner.is_empty() {
137 continue;
138 }
139
140 let bnode = fresh_blank_label(&source.0);
141
142 let Some(replace_range) = offsets_to_range(bspan.start, bspan.end, &rope.0) else {
144 continue;
145 };
146 let replace_edit = TextEdit {
147 range: replace_range,
148 new_text: bnode.clone(),
149 };
150
151 let end_offset = source.0.len();
153 let Some(end_pos) = offset_to_position(end_offset, &rope.0) else {
154 continue;
155 };
156 let needs_leading_newline = !source.0.ends_with('\n');
157 let append_edit = TextEdit {
158 range: Range::new(end_pos, end_pos),
159 new_text: format!(
160 "{}{} {} .\n",
161 if needs_leading_newline { "\n" } else { "" },
162 bnode,
163 inner
164 ),
165 };
166
167 let mut changes = HashMap::new();
168 changes.insert(label.0.clone(), vec![replace_edit, append_edit]);
169
170 req.0.push(CodeAction {
171 title: String::from("Extract blank node into named blank node"),
172 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
173 edit: Some(WorkspaceEdit {
174 changes: Some(changes),
175 ..Default::default()
176 }),
177 ..Default::default()
178 });
179 }
180}
181
182fn find_named_at(turtle: &Turtle, offset: usize) -> Option<(String, StdRange<usize>)> {
186 fn walk(
187 term: &Spanned<Term>,
188 offset: usize,
189 best: &mut Option<(String, StdRange<usize>)>,
190 ) {
191 let span = term.span();
192 if offset < span.start || offset > span.end {
193 return;
194 }
195 match term.value() {
196 Term::BlankNode(BlankNode::Named(name, _)) => {
197 *best = Some((name.clone(), span.clone()));
198 }
199 Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
200 for po in pos {
201 walk(&po.value().predicate, offset, best);
202 for object in &po.value().object {
203 walk(object, offset, best);
204 }
205 }
206 }
207 Term::Collection(items) => {
208 for item in items {
209 walk(item, offset, best);
210 }
211 }
212 _ => {}
213 }
214 }
215
216 let mut best = None;
217 for triple in &turtle.triples {
218 let triple = triple.value();
219 walk(&triple.subject, offset, &mut best);
220 for po in &triple.po {
221 walk(&po.value().predicate, offset, &mut best);
222 for object in &po.value().object {
223 walk(object, offset, &mut best);
224 }
225 }
226 }
227 best
228}
229
230fn collect_named_refs(term: &Spanned<Term>, name: &str, out: &mut Vec<StdRange<usize>>) {
233 match term.value() {
234 Term::BlankNode(BlankNode::Named(n, _)) if n == name => out.push(term.span().clone()),
235 Term::BlankNode(BlankNode::Unnamed(pos, _, _)) => {
236 for po in pos {
237 collect_named_refs(&po.value().predicate, name, out);
238 for object in &po.value().object {
239 collect_named_refs(object, name, out);
240 }
241 }
242 }
243 Term::Collection(items) => {
244 for item in items {
245 collect_named_refs(item, name, out);
246 }
247 }
248 _ => {}
249 }
250}
251
252pub fn inline_blank_node(
270 mut query: Query<(
271 &Element,
272 &Source,
273 &RopeC,
274 &Label,
275 &PositionComponent,
276 &DynLang,
277 &mut CodeActionRequest,
278 )>,
279 config: Res<ServerConfig>,
280) {
281 if config.config.local.is_disabled(Disabled::CodeActionBlankNodeRefactor) {
282 return;
283 }
284 for (element, source, rope, label, position, lang, mut req) in &mut query {
285 if !lang.0.blank_node_code_actions() {
286 continue;
287 }
288 let Some(offset) = position_to_offset(position.0, &rope.0) else {
289 continue;
290 };
291 let turtle: &Turtle = element.value();
292
293 let Some((name, _)) = find_named_at(turtle, offset) else {
294 continue;
295 };
296
297 let mut def: Option<(usize, StdRange<usize>)> = None;
300 let mut refs: Vec<StdRange<usize>> = Vec::new();
301 for (idx, triple) in turtle.triples.iter().enumerate() {
302 let tvalue = triple.value();
303 let is_def = matches!(
304 tvalue.subject.value(),
305 Term::BlankNode(BlankNode::Named(n, _)) if *n == name
306 );
307 if is_def && def.is_none() {
308 def = Some((idx, tvalue.subject.span().clone()));
309 } else {
310 collect_named_refs(&tvalue.subject, &name, &mut refs);
311 }
312 for po in &tvalue.po {
315 collect_named_refs(&po.value().predicate, &name, &mut refs);
316 for object in &po.value().object {
317 collect_named_refs(object, &name, &mut refs);
318 }
319 }
320 }
321
322 let Some((def_idx, _)) = def else {
324 continue;
325 };
326 if refs.len() != 1 {
327 continue;
328 }
329 let ref_span = refs.into_iter().next().unwrap();
330
331 let def_triple = turtle.triples[def_idx].value();
334 let def_span = turtle.triples[def_idx].span();
335 let inner_start = def_triple.subject.span().end;
336 let Some(inner_raw) = source.0.get(inner_start..def_span.end) else {
337 continue;
338 };
339 let inner = inner_raw.trim();
340 let inner = inner.strip_suffix('.').unwrap_or(inner).trim();
341 if inner.is_empty() {
342 continue;
343 }
344
345 let mut del_start = def_span.start;
348 let mut del_end = def_span.end;
349 let bytes = source.0.as_bytes();
350 while del_end < bytes.len() && matches!(bytes[del_end], b' ' | b'\t') {
351 del_end += 1;
352 }
353 if del_end < bytes.len() && bytes[del_end] == b'\r' {
354 del_end += 1;
355 }
356 if del_end < bytes.len() && bytes[del_end] == b'\n' {
357 del_end += 1;
358 }
359 while del_start > 0 && matches!(bytes[del_start - 1], b' ' | b'\t') {
360 del_start -= 1;
361 }
362
363 if del_start < ref_span.end && ref_span.start < del_end {
366 continue;
367 }
368
369 let Some(ref_range) = offsets_to_range(ref_span.start, ref_span.end, &rope.0) else {
370 continue;
371 };
372 let Some(del_range) = offsets_to_range(del_start, del_end, &rope.0) else {
373 continue;
374 };
375
376 let replace_edit = TextEdit {
377 range: ref_range,
378 new_text: format!("[ {} ]", inner),
379 };
380 let delete_edit = TextEdit {
381 range: del_range,
382 new_text: String::new(),
383 };
384
385 let mut changes = HashMap::new();
386 changes.insert(label.0.clone(), vec![replace_edit, delete_edit]);
387
388 req.0.push(CodeAction {
389 title: String::from("Inline named blank node"),
390 kind: Some(CodeActionKind::REFACTOR_INLINE),
391 edit: Some(WorkspaceEdit {
392 changes: Some(changes),
393 ..Default::default()
394 }),
395 ..Default::default()
396 });
397 }
398}
399
400