Skip to main content

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}