yaml_edit/nodes/mapping/movements.rs
1//! `Mapping::move_after` / `move_before` -- pull a key out of its
2//! current position and re-insert it at a new position, along with
3//! their shared `move_flow_around` helper for flow-style mappings.
4//!
5//! Split out of `nodes/mapping/mod.rs` unchanged.
6
7use super::helpers::FlowInsertPos;
8use super::{key_content_matches, Mapping, MappingEntry};
9use crate::lex::SyntaxKind;
10use crate::nodes::{ensure_own_trailing_newline, fresh_token, SyntaxNode};
11use crate::yaml::Document;
12use rowan::ast::AstNode;
13use rowan::GreenNodeBuilder;
14
15impl Mapping {
16 /// Move-or-insert `new_key: new_value` relative to `ref_key` in a
17 /// flow-style mapping. Shared body of the flow branches in
18 /// `move_before_impl` / `move_after_impl`. Returns `false` if
19 /// `ref_key` is not present (nothing to anchor on).
20 fn move_flow_around(
21 &self,
22 ref_key: &impl crate::AsYaml,
23 new_key: &impl crate::AsYaml,
24 new_value: &impl crate::AsYaml,
25 where_at: fn(SyntaxNode) -> FlowInsertPos,
26 ) -> bool {
27 let Some(target) = self.find_entry_by_key(ref_key) else {
28 return false;
29 };
30 if self
31 .find_entry_by_key(new_key)
32 .is_some_and(|e| e.syntax() == target.syntax())
33 {
34 self.set(new_key, new_value);
35 return true;
36 }
37 // Contract: move, not duplicate. Drop any prior entry with the
38 // same key first.
39 if let Some(existing) = self.find_entry_by_key(new_key) {
40 if let Some(idx) = self
41 .0
42 .children_with_tokens()
43 .position(|c| c.as_node() == Some(existing.syntax()))
44 {
45 self.0.splice_children(idx..idx + 1, vec![]);
46 }
47 }
48 let entry = MappingEntry::new_at_indent(
49 new_key,
50 new_value,
51 /* flow_context */ true,
52 self.uses_explicit_keys(),
53 self.detect_indentation_level(),
54 );
55 self.insert_flow_entry_cst_at(&entry.0, where_at(target.syntax().clone()));
56 true
57 }
58 /// Move a key-value pair to immediately after an existing key.
59 ///
60 /// If `new_key` already exists in the mapping, it is first **removed** from its
61 /// current position and then re-inserted after `after_key` with the new value -
62 /// so the key ends up at the requested position regardless of where it was before.
63 ///
64 /// If `after_key` is not found, returns `false` and leaves the mapping unchanged.
65 /// Returns `true` on success.
66 ///
67 /// Use [`insert_after`](Self::insert_after) if you want existing entries to be
68 /// updated in-place rather than moved.
69 pub fn move_after(
70 &self,
71 after_key: impl crate::AsYaml,
72 new_key: impl crate::AsYaml,
73 new_value: impl crate::AsYaml,
74 ) -> bool {
75 self.move_after_impl(after_key, new_key, new_value)
76 }
77
78 /// Internal implementation for move_after
79 fn move_after_impl(
80 &self,
81 after_key: impl crate::AsYaml,
82 new_key: impl crate::AsYaml,
83 new_value: impl crate::AsYaml,
84 ) -> bool {
85 let Some(target) = self.find_entry_by_key(&after_key) else {
86 return false;
87 };
88 if self
89 .find_entry_by_key(&new_key)
90 .is_some_and(|e| e.syntax() == target.syntax())
91 {
92 self.set(&new_key, &new_value);
93 return true;
94 }
95 if self.is_flow_style() {
96 return self.move_flow_around(&after_key, &new_key, &new_value, FlowInsertPos::After);
97 }
98
99 let children: Vec<_> = self.0.children_with_tokens().collect();
100 let mut insert_position = None;
101 let mut found_key = false;
102 let mut last_value_end = 0;
103
104 // First, check if the new key already exists and remove it
105 let mut i = 0;
106 let mut removed_existing = false;
107 while i < children.len() {
108 if let Some(node) = children[i].as_node() {
109 if node.kind() == SyntaxKind::MAPPING_ENTRY {
110 // Look inside the MAPPING_ENTRY for the KEY
111 for key_child in node.children() {
112 if key_child.kind() == SyntaxKind::KEY
113 && key_content_matches(&key_child, &new_key)
114 {
115 // Found existing key, remove this entire MAPPING_ENTRY
116 let mut remove_range = i..i + 1;
117
118 // Also remove any trailing newline
119 if i + 1 < children.len() {
120 if let Some(token) = children[i + 1].as_token() {
121 if token.kind() == SyntaxKind::NEWLINE {
122 remove_range = i..i + 2;
123 }
124 }
125 }
126
127 self.0.splice_children(remove_range, vec![]);
128 removed_existing = true;
129 break;
130 }
131 }
132 if removed_existing {
133 // Need to refresh children list after removal
134 break;
135 }
136 }
137 }
138 if !removed_existing {
139 i += 1;
140 }
141 }
142
143 // If we removed an existing key, refresh the children list
144 let children = if removed_existing {
145 self.0.children_with_tokens().collect()
146 } else {
147 children
148 };
149
150 // Find the position after the specified key's value
151 for (i, child) in children.iter().enumerate() {
152 if let Some(node) = child.as_node() {
153 if node.kind() == SyntaxKind::MAPPING_ENTRY {
154 if found_key {
155 // Check if this MAPPING_ENTRY is at the root level
156 // Root level means it's not preceded by INDENT
157 let is_root_level = if i > 0 {
158 children
159 .get(i - 1)
160 .and_then(|c| c.as_token())
161 .map_or(true, |t| t.kind() != SyntaxKind::INDENT)
162 } else {
163 true
164 };
165
166 if is_root_level {
167 insert_position = Some(i);
168 break;
169 }
170 }
171 // Look inside the MAPPING_ENTRY for the KEY
172 for key_child in node.children() {
173 if key_child.kind() == SyntaxKind::KEY
174 && key_content_matches(&key_child, &after_key)
175 {
176 found_key = true;
177 last_value_end = i + 1; // After this entire MAPPING_ENTRY
178 break;
179 }
180 }
181 } else if node.kind() == SyntaxKind::KEY {
182 if key_content_matches(node, &after_key) {
183 found_key = true;
184 }
185 } else if node.kind() == SyntaxKind::SCALAR {
186 // For SCALAR nodes that might be keys
187 if key_content_matches(node, &after_key) && !found_key {
188 // This is likely the key we're looking for
189 found_key = true;
190 // Look ahead for the value
191 for (j, child_j) in children[(i + 1)..].iter().enumerate() {
192 if let Some(n) = child_j.as_node() {
193 if n.kind() == SyntaxKind::VALUE || n.kind() == SyntaxKind::SCALAR {
194 last_value_end = i + 1 + j + 1;
195 break;
196 }
197 }
198 }
199 }
200 } else if node.kind() == SyntaxKind::VALUE && found_key {
201 // We're at the value of the found key
202 last_value_end = i + 1;
203 }
204 } else if let Some(token) = child.as_token() {
205 if found_key && token.kind() == SyntaxKind::COMMENT {
206 // Check if this comment is at the top level (not indented)
207 // Top-level comments can be preceded by:
208 // 1. NEWLINE token (traditional case)
209 // 2. MAPPING_ENTRY node (when all newlines are inside the entry)
210 if i > 0 {
211 if let Some(prev) = children.get(i - 1) {
212 let is_top_level = if let Some(prev_token) = prev.as_token() {
213 // Preceded by token - check if it's NEWLINE (not INDENT)
214 prev_token.kind() == SyntaxKind::NEWLINE
215 } else if let Some(prev_node) = prev.as_node() {
216 // Preceded by node - check if it's a MAPPING_ENTRY
217 // (means all newlines were inside the entry)
218 prev_node.kind() == SyntaxKind::MAPPING_ENTRY
219 } else {
220 false
221 };
222
223 if is_top_level {
224 // Top-level comment - insert before it
225 insert_position = Some(i);
226 break;
227 }
228 }
229 }
230 } else if found_key && token.kind() == SyntaxKind::NEWLINE {
231 // Check if this is a root-level newline (not inside nested content)
232 // Root-level means not preceded by INDENT
233 let is_root_level = if i > 0 {
234 children
235 .get(i - 1)
236 .and_then(|c| c.as_token())
237 .map_or(true, |t| t.kind() != SyntaxKind::INDENT)
238 } else {
239 true
240 };
241
242 if is_root_level && i + 1 < children.len() {
243 if let Some(next) = children.get(i + 1) {
244 if let Some(next_token) = next.as_token() {
245 if next_token.kind() == SyntaxKind::NEWLINE
246 || next_token.kind() == SyntaxKind::COMMENT
247 {
248 // Blank line or comment follows - insert before
249 // them so the new entry is right after the target
250 // key and blank lines are preserved before the
251 // next key
252 insert_position = Some(i);
253 break;
254 }
255 } else if next.as_node().is_some() {
256 // Node follows (likely MAPPING_ENTRY) - insert before
257 // this separator newline so the new entry is right
258 // after the target key
259 insert_position = Some(i);
260 break;
261 }
262 }
263 }
264 }
265 }
266 }
267
268 // If we didn't find a newline but found the key, insert after the value
269 if insert_position.is_none() && found_key && last_value_end > 0 {
270 insert_position = Some(last_value_end);
271 }
272
273 if let Some(pos) = insert_position {
274 // Create new elements for the key-value pair
275 let mut new_elements = Vec::new();
276
277 // Check if the previous entry has a trailing newline and add one if needed
278 if pos > 0 {
279 // Look backwards for the last MAPPING_ENTRY
280 if let Some(prev_entry) = children[..pos].iter().rev().find_map(|child| {
281 child
282 .as_node()
283 .filter(|n| n.kind() == SyntaxKind::MAPPING_ENTRY)
284 }) {
285 // Terminate the previous entry so the moved one starts
286 // on its own line. The entry needs its own NEWLINE: the
287 // insert position was computed separately, so a sibling
288 // NEWLINE may well sit past it.
289 ensure_own_trailing_newline(prev_entry);
290 }
291 }
292
293 // Add indentation if needed
294 // Check if we're inserting at root level by looking at the previous element
295 let needs_indent = pos > 0
296 && children
297 .get(pos - 1)
298 .and_then(|c| c.as_token())
299 .is_some_and(|t| t.kind() == SyntaxKind::INDENT);
300
301 if needs_indent {
302 let indent_level = self.detect_indentation_level();
303 if indent_level > 0 {
304 new_elements
305 .push(fresh_token(SyntaxKind::INDENT, &" ".repeat(indent_level)).into());
306 }
307 }
308
309 // Create the MAPPING_ENTRY node
310 let (entry, _has_trailing_newline) = self.create_mapping_entry(&new_key, &new_value);
311
312 // Add the new entry (which already has its own trailing newline)
313 new_elements.push(entry.into());
314
315 // Splice in the new elements
316 self.0.splice_children(pos..pos, new_elements);
317 true
318 } else {
319 false
320 }
321 }
322 /// Move a key-value pair to immediately before an existing key.
323 ///
324 /// If `new_key` already exists in the mapping, it is first **removed** from its
325 /// current position and then re-inserted before `before_key` with the new value.
326 ///
327 /// If `before_key` is not found, returns `false` and leaves the mapping unchanged.
328 /// Returns `true` on success.
329 ///
330 /// Use [`insert_before`](Self::insert_before) if you want existing entries to be
331 /// updated in-place rather than moved.
332 pub fn move_before(
333 &self,
334 before_key: impl crate::AsYaml,
335 new_key: impl crate::AsYaml,
336 new_value: impl crate::AsYaml,
337 ) -> bool {
338 self.move_before_impl(before_key, new_key, new_value)
339 }
340
341 /// Internal implementation for move_before
342 fn move_before_impl(
343 &self,
344 before_key: impl crate::AsYaml,
345 new_key: impl crate::AsYaml,
346 new_value: impl crate::AsYaml,
347 ) -> bool {
348 let Some(target) = self.find_entry_by_key(&before_key) else {
349 return false;
350 };
351 if self
352 .find_entry_by_key(&new_key)
353 .is_some_and(|e| e.syntax() == target.syntax())
354 {
355 self.set(&new_key, &new_value);
356 return true;
357 }
358 if self.is_flow_style() {
359 return self.move_flow_around(&before_key, &new_key, &new_value, FlowInsertPos::Before);
360 }
361
362 let children: Vec<_> = self.0.children_with_tokens().collect();
363 let mut insert_position = None;
364
365 // First, check if the new key already exists and remove it
366 let mut i = 0;
367 let mut removed_existing = false;
368 while i < children.len() {
369 if let Some(node) = children[i].as_node() {
370 if node.kind() == SyntaxKind::MAPPING_ENTRY {
371 // Look inside the MAPPING_ENTRY for the KEY
372 for key_child in node.children() {
373 if key_child.kind() == SyntaxKind::KEY
374 && key_content_matches(&key_child, &new_key)
375 {
376 // Found existing key, remove this entire MAPPING_ENTRY
377 let mut remove_range = i..i + 1;
378
379 // Also remove any trailing newline
380 if i + 1 < children.len() {
381 if let Some(token) = children[i + 1].as_token() {
382 if token.kind() == SyntaxKind::NEWLINE {
383 remove_range = i..i + 2;
384 }
385 }
386 }
387
388 self.0.splice_children(remove_range, vec![]);
389 removed_existing = true;
390 break;
391 }
392 }
393 if removed_existing {
394 // Need to refresh children list after removal
395 break;
396 }
397 } else if (node.kind() == SyntaxKind::KEY || node.kind() == SyntaxKind::SCALAR)
398 && key_content_matches(node, &new_key)
399 {
400 // Found existing key, find its VALUE node and replace just that
401 // Look for colon, then VALUE node
402 for (offset, child_j) in children[(i + 1)..].iter().enumerate() {
403 if let Some(node) = child_j.as_node() {
404 if node.kind() == SyntaxKind::VALUE {
405 // Found the VALUE node to replace
406 // Build new VALUE node using the helper
407 let mut value_builder = GreenNodeBuilder::new();
408 Document::build_value_content(&mut value_builder, &new_value, 2);
409 let new_value_node =
410 SyntaxNode::new_root_mut(value_builder.finish());
411
412 // Replace just the VALUE node
413 let j = i + 1 + offset;
414 self.0
415 .splice_children(j..j + 1, vec![new_value_node.into()]);
416 return true;
417 }
418 }
419 }
420 // If no VALUE node found, something's wrong with the structure
421 return false;
422 }
423 }
424 if !removed_existing {
425 i += 1;
426 }
427 }
428
429 // If we removed an existing key, refresh the children list
430 let children = if removed_existing {
431 self.0.children_with_tokens().collect()
432 } else {
433 children
434 };
435
436 // Find the position before the specified key
437 for (i, child) in children.iter().enumerate() {
438 if let Some(node) = child.as_node() {
439 if node.kind() == SyntaxKind::MAPPING_ENTRY {
440 // Look inside the MAPPING_ENTRY for the KEY
441 for key_child in node.children() {
442 if key_child.kind() == SyntaxKind::KEY
443 && key_content_matches(&key_child, &before_key)
444 {
445 // Found the key, insert before this MAPPING_ENTRY
446 let mut line_start = i;
447 for j in (0..i).rev() {
448 if let Some(token) = children[j].as_token() {
449 if token.kind() == SyntaxKind::NEWLINE {
450 line_start = j + 1;
451 break;
452 }
453 }
454 }
455 insert_position = Some(line_start);
456 break;
457 }
458 }
459 } else if (node.kind() == SyntaxKind::KEY || node.kind() == SyntaxKind::SCALAR)
460 && key_content_matches(node, &before_key)
461 {
462 // Found the key, insert before it
463 // Look back to find the start of this line
464 let mut line_start = i;
465 for j in (0..i).rev() {
466 if let Some(token) = children[j].as_token() {
467 if token.kind() == SyntaxKind::NEWLINE {
468 line_start = j + 1;
469 break;
470 }
471 }
472 }
473 insert_position = Some(line_start);
474 break;
475 }
476 }
477 }
478
479 if let Some(pos) = insert_position {
480 // Create new AST elements for the key-value pair
481 // Build the complete key-value entry as separate nodes/tokens
482
483 // Build each element as a SyntaxNode/Token
484 let mut new_elements = Vec::new();
485
486 // Create the MAPPING_ENTRY node
487 let (entry, _has_trailing_newline) = self.create_mapping_entry(&new_key, &new_value);
488 new_elements.push(entry.into());
489
490 // Note: create_mapping_entry already adds a trailing newline to the MAPPING_ENTRY
491 // (newline ownership model), so we don't add an extra one here
492
493 // Splice in the new elements
494 self.0.splice_children(pos..pos, new_elements);
495 true
496 } else {
497 false
498 }
499 }
500}