Skip to main content

yaml_rt_core/
edit.rs

1use std::cmp::Ordering;
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4
5use crate::fragment::indent_text;
6use crate::pointer::parse_sequence_index;
7use crate::{
8    CollectionStyle, Diagnostic, DiagnosticKind, FragmentError, JsonPointer, NodeId, PointerError,
9    ResolvedScalar, SemanticKind, SemanticValueError, Span, YamlDoc, YamlError, YamlFragment,
10    YamlScalarStyle, resolve_scalar, semantically_equal, strip_inline_comment,
11};
12
13/// Failure while applying a pointer-addressed YAML edit.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct YamlEditError {
16    message: String,
17    source_span: Option<Span>,
18}
19
20/// A transactional, source-preserving editor for one YAML sequence.
21pub struct SequenceEditor<'a> {
22    doc: &'a mut YamlDoc,
23    sequence: NodeId,
24}
25
26impl SequenceEditor<'_> {
27    /// Stably sorts items using semantic inspection of their lossless nodes.
28    pub fn sort_by(
29        self,
30        mut compare: impl FnMut(&YamlDoc, NodeId, NodeId) -> Ordering,
31    ) -> Result<(), YamlEditError> {
32        let sequence = self.sequence;
33        self.doc.transaction(|work| {
34            let original = work.sequence_items(sequence).collect::<Vec<_>>();
35            let mut sorted = original.clone();
36            sorted.sort_by(|left, right| compare(work, *left, *right));
37            if sorted == original {
38                return Ok(());
39            }
40            let order = sorted
41                .iter()
42                .map(|item| {
43                    original
44                        .iter()
45                        .position(|original| original == item)
46                        .expect("sorted items originate in the sequence")
47                })
48                .collect::<Vec<_>>();
49            work.queue_sequence_reorder(sequence, &order)
50        })
51    }
52
53    /// Inserts `value` before the item at `index`, or appends at `len`.
54    pub fn insert(self, index: usize, value: &YamlFragment) -> Result<(), YamlEditError> {
55        let sequence = self.sequence;
56        self.doc.transaction(|work| {
57            let len = work.sequence_items(sequence).count();
58            if index > len {
59                return Err(work.sequence_index_error(sequence, index, len, true));
60            }
61            work.queue_sequence_insert(sequence, index, value)
62        })
63    }
64
65    /// Moves an existing item to its index in the finished sequence.
66    pub fn move_item(self, from: usize, to: usize) -> Result<(), YamlEditError> {
67        let sequence = self.sequence;
68        self.doc.transaction(|work| {
69            let len = work.sequence_items(sequence).count();
70            if from >= len {
71                return Err(work.sequence_index_error(sequence, from, len, false));
72            }
73            if to >= len {
74                return Err(work.sequence_index_error(sequence, to, len, false));
75            }
76            if from == to {
77                return Ok(());
78            }
79            let mut order = (0..len).collect::<Vec<_>>();
80            let moved = order.remove(from);
81            order.insert(to, moved);
82            work.queue_sequence_reorder(sequence, &order)
83        })
84    }
85
86    /// Retains only items for which `predicate` returns `true`.
87    ///
88    /// The predicate is called once per original item in source order. Removed
89    /// items take their attached comments and other entry trivia with them.
90    pub fn retain(
91        self,
92        mut predicate: impl FnMut(&YamlDoc, NodeId) -> bool,
93    ) -> Result<(), YamlEditError> {
94        let sequence = self.sequence;
95        self.doc.transaction(|work| {
96            let items = work.sequence_items(sequence).collect::<Vec<_>>();
97            let entries = items
98                .iter()
99                .filter(|item| !predicate(work, **item))
100                .filter_map(|item| work.containing_entry(*item))
101                .collect::<Vec<_>>();
102            work.remove_collection_entries(sequence, &entries)
103                .map_err(Into::into)
104        })
105    }
106}
107
108impl YamlEditError {
109    pub(crate) fn new(message: impl Into<String>) -> Self {
110        Self {
111            message: message.into(),
112            source_span: None,
113        }
114    }
115
116    fn with_source_span(mut self, source_span: Span) -> Self {
117        self.source_span = Some(source_span);
118        self
119    }
120
121    /// Returns the relevant YAML source span for the edit failure.
122    #[must_use]
123    pub const fn source_span(&self) -> Option<Span> {
124        self.source_span
125    }
126
127    pub(crate) fn into_yaml_error(self) -> YamlError {
128        let span = self.source_span.unwrap_or_else(|| Span::empty(0));
129        YamlError::new(
130            Diagnostic::new(DiagnosticKind::Emitter, self.message, span)
131                .with_expected("a source-preserving YAML edit"),
132        )
133    }
134}
135
136impl fmt::Display for YamlEditError {
137    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138        formatter.write_str(&self.message)
139    }
140}
141
142impl std::error::Error for YamlEditError {}
143
144impl From<PointerError> for YamlEditError {
145    fn from(error: PointerError) -> Self {
146        let source_span = error.source_span();
147        let mut converted = Self::new(error.to_string());
148        if let Some(span) = source_span {
149            converted = converted.with_source_span(span);
150        }
151        converted
152    }
153}
154
155impl From<FragmentError> for YamlEditError {
156    fn from(error: FragmentError) -> Self {
157        Self::new(error.to_string())
158    }
159}
160
161impl From<YamlError> for YamlEditError {
162    fn from(error: YamlError) -> Self {
163        Self::new(error.to_string())
164    }
165}
166
167impl From<SemanticValueError> for YamlEditError {
168    fn from(error: SemanticValueError) -> Self {
169        Self::new(error.to_string())
170    }
171}
172
173enum AddLocation {
174    Root(NodeId),
175    Mapping {
176        mapping: NodeId,
177        existing: Option<NodeId>,
178        key: String,
179    },
180    Sequence {
181        sequence: NodeId,
182        index: usize,
183    },
184}
185
186#[derive(Clone, Copy)]
187struct RenameTarget {
188    mapping: NodeId,
189    key: NodeId,
190}
191
192impl YamlDoc {
193    /// Creates a focused editor for `sequence`.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error when the node is missing or is not a YAML sequence.
198    pub fn sequence_editor(
199        &mut self,
200        sequence: NodeId,
201    ) -> Result<SequenceEditor<'_>, YamlEditError> {
202        let node = self
203            .node(sequence)
204            .ok_or_else(|| YamlEditError::new("sequence editor target does not exist"))?;
205        if !matches!(
206            self.semantic_kind(sequence),
207            Some(SemanticKind::Sequence { .. })
208        ) {
209            return Err(
210                YamlEditError::new("sequence editor target is not a sequence")
211                    .with_source_span(node.span),
212            );
213        }
214        Ok(SequenceEditor {
215            doc: self,
216            sequence,
217        })
218    }
219
220    fn sequence_index_error(
221        &self,
222        sequence: NodeId,
223        index: usize,
224        len: usize,
225        allow_end: bool,
226    ) -> YamlEditError {
227        let expected = if allow_end {
228            format!("a sequence insertion index from 0 through {len}")
229        } else if len == 0 {
230            "an item index in a nonempty sequence".to_owned()
231        } else {
232            format!("a sequence item index from 0 through {}", len - 1)
233        };
234        YamlEditError::new(format!(
235            "sequence index {index} is out of bounds; expected {expected}"
236        ))
237        .with_source_span(
238            self.node(sequence)
239                .map_or_else(|| Span::empty(0), |node| node.span),
240        )
241    }
242
243    fn queue_sequence_reorder(
244        &mut self,
245        sequence: NodeId,
246        order: &[usize],
247    ) -> Result<(), YamlEditError> {
248        let entries = self
249            .children(sequence)
250            .filter(|node| self.containing_entry_child(*node))
251            .collect::<Vec<_>>();
252        if entries.len() != order.len() || order.iter().any(|index| *index >= entries.len()) {
253            return Err(YamlEditError::new("invalid sequence item ordering"));
254        }
255        let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
256            return Err(YamlEditError::new(
257                "sequence reorder target is not a sequence",
258            ));
259        };
260        match style {
261            CollectionStyle::Block => {
262                let spans = entries
263                    .iter()
264                    .map(|entry| self.block_collection_entry_removal_span(sequence, *entry))
265                    .collect::<Result<Vec<_>, _>>()?;
266                let chunks = spans
267                    .iter()
268                    .map(|span| self.source().slice(*span).to_owned())
269                    .collect::<Vec<_>>();
270                let span = Span::new(spans[0].start, spans.last().expect("entries exist").end);
271                let replacement = order.iter().map(|index| chunks[*index].as_str()).collect();
272                self.queue_edit(span, replacement)?;
273            }
274            CollectionStyle::Flow => {
275                let sequence_span = self.expect_node(sequence)?.span;
276                let open = self
277                    .source()
278                    .slice(sequence_span)
279                    .find('[')
280                    .map(|offset| sequence_span.start as usize + offset + 1)
281                    .ok_or_else(|| {
282                        YamlEditError::new("flow sequence opening bracket is missing")
283                    })?;
284                let close = closing_delimiter_offset(self, sequence_span, ']')?;
285                let mut chunks = Vec::with_capacity(entries.len());
286                let mut leading = &self.source().as_str()
287                    [open..self.expect_node(entries[0])?.span.start as usize];
288                for (index, entry) in entries.iter().copied().enumerate() {
289                    let entry_span = self.expect_node(entry)?.span;
290                    let next_start = entries.get(index + 1).map_or(close, |next| {
291                        self.expect_node(*next).unwrap().span.start as usize
292                    });
293                    let gap = &self.source().as_str()[entry_span.end as usize..next_start];
294                    let (trailing, next_leading) = if index + 1 < entries.len() {
295                        let comma = gap.find(',').ok_or_else(|| {
296                            YamlEditError::new("flow sequence separator is missing")
297                        })?;
298                        (&gap[..comma], &gap[comma + 1..])
299                    } else {
300                        (gap, "")
301                    };
302                    chunks.push(format!(
303                        "{leading}{}{trailing}",
304                        self.source().slice(entry_span)
305                    ));
306                    leading = next_leading;
307                }
308                let replacement = order
309                    .iter()
310                    .map(|index| chunks[*index].as_str())
311                    .collect::<Vec<_>>()
312                    .join(",");
313                self.queue_edit(Span::from_usize(open, close), replacement)?;
314            }
315        }
316        Ok(())
317    }
318
319    /// Applies RFC 6902 `add` semantics at a JSON Pointer destination.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error when the document or destination does not exist, the
324    /// destination cannot accept the value, or the edit cannot be emitted.
325    pub fn add_at(
326        &mut self,
327        document: usize,
328        pointer: &JsonPointer,
329        value: &YamlFragment,
330    ) -> Result<(), YamlEditError> {
331        self.transaction(|work| {
332            let location = work.resolve_add_location(document, pointer)?;
333            work.queue_add(location, value)
334        })
335    }
336
337    /// Removes an existing value. Removing a document root is unsupported.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error when the document or target does not exist, the target
342    /// is a document root, or removing it would invalidate the document.
343    pub fn remove_at(
344        &mut self,
345        document: usize,
346        pointer: &JsonPointer,
347    ) -> Result<(), YamlEditError> {
348        if pointer.is_root() {
349            return Err(YamlEditError::new(
350                "removing a YAML document root is not supported",
351            ));
352        }
353        self.transaction(|work| {
354            let target = work.resolve_pointer(document, pointer)?;
355            work.queue_value_removal(target)
356        })
357    }
358
359    /// Replaces an existing value while preserving its surrounding syntax.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error when the document or target does not exist or the
364    /// replacement cannot be emitted at that location.
365    pub fn replace_at(
366        &mut self,
367        document: usize,
368        pointer: &JsonPointer,
369        value: &YamlFragment,
370    ) -> Result<(), YamlEditError> {
371        self.transaction(|work| {
372            let target = work.resolve_pointer(document, pointer)?;
373            work.queue_fragment_replacement(target, value)
374        })
375    }
376
377    /// Moves a value using RFC 6902 remove-then-add semantics.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error when either pointer is invalid for the document, the
382    /// move would be recursive, or anchors, aliases, or syntax prevent it.
383    pub fn move_at(
384        &mut self,
385        document: usize,
386        from: &JsonPointer,
387        path: &JsonPointer,
388    ) -> Result<(), YamlEditError> {
389        if from == path {
390            return Ok(());
391        }
392        if from.is_proper_prefix_of(path) {
393            return Err(YamlEditError::new(
394                "move source must not be a proper prefix of its destination",
395            ));
396        }
397        let mut work = self.clone();
398        let source = work.resolve_pointer(document, from)?;
399        if work.anchor_has_external_alias(source) {
400            return Err(YamlEditError::new(
401                "cannot move anchored subtree because an alias outside it depends on that anchor",
402            ));
403        }
404        let fragment = YamlFragment::from_document_node(&work, source)?;
405        if from.is_root() {
406            return Err(YamlEditError::new(
407                "a document root cannot be moved into another location",
408            ));
409        }
410        work.queue_value_removal(source)?;
411        work.commit_edits()?;
412        let location = work.resolve_add_location(document, path)?;
413        work.queue_add(location, &fragment)?;
414        work.commit_edits()?;
415        *self = work;
416        Ok(())
417    }
418
419    /// Deep-copies a value using RFC 6902 `copy` semantics.
420    ///
421    /// # Errors
422    ///
423    /// Returns an error when either pointer is invalid for the document, the
424    /// source contains an anchor, or the copied value cannot be inserted.
425    pub fn copy_at(
426        &mut self,
427        document: usize,
428        from: &JsonPointer,
429        path: &JsonPointer,
430    ) -> Result<(), YamlEditError> {
431        let source = self.resolve_pointer(document, from)?;
432        let fragment = YamlFragment::from_document_node(self, source)?;
433        if fragment.contains_anchor() {
434            return Err(YamlEditError::new(format!(
435                "cannot copy {:?}: subtree contains an anchor",
436                from.as_str()
437            )));
438        }
439        self.add_at(document, path, &fragment)
440    }
441
442    /// Compares a pointer-selected target with a YAML value.
443    ///
444    /// # Errors
445    ///
446    /// Returns an error when the document or target does not exist or either
447    /// value cannot be compared using RFC 6902 equality.
448    pub fn test_at(
449        &self,
450        document: usize,
451        pointer: &JsonPointer,
452        value: &YamlFragment,
453    ) -> Result<bool, YamlEditError> {
454        let target = self.resolve_pointer(document, pointer)?;
455        semantically_equal(self, target, value.document(), value.root()).map_err(Into::into)
456    }
457
458    /// Renames the mapping key that owns a pointer-selected value.
459    ///
460    /// The operation is transactional and changes only the key scalar spelling.
461    /// The pointer must select a mapping member; document roots and sequence
462    /// elements do not have a key to rename.
463    ///
464    /// # Errors
465    ///
466    /// Returns an error when the pointer does not select a supported string key,
467    /// the destination key would collide with another mapping member, or the
468    /// edited document cannot be emitted.
469    pub fn rename_key_at(
470        &mut self,
471        document: usize,
472        pointer: &JsonPointer,
473        new_key: &str,
474    ) -> Result<(), YamlEditError> {
475        self.rename_keys_at(document, std::slice::from_ref(pointer), new_key)
476    }
477
478    /// Renames the mapping keys that own several pointer-selected values.
479    ///
480    /// Targets are resolved against the original document and duplicate source
481    /// key nodes are edited once. All collision checks and edits are applied as
482    /// one transaction. An empty pointer slice succeeds without changing the
483    /// document.
484    ///
485    /// # Errors
486    ///
487    /// Returns an error when any pointer does not select a supported string key,
488    /// any affected mapping would contain duplicate final keys, or the edited
489    /// document cannot be emitted.
490    pub fn rename_keys_at(
491        &mut self,
492        document: usize,
493        pointers: &[JsonPointer],
494        new_key: &str,
495    ) -> Result<(), YamlEditError> {
496        if pointers.is_empty() {
497            return Ok(());
498        }
499
500        let mut work = self.clone();
501        let mut seen_keys = HashSet::new();
502        let mut targets = Vec::new();
503        for pointer in pointers {
504            let target = work.resolve_rename_target(document, pointer)?;
505            if seen_keys.insert(target.key) {
506                targets.push(target);
507            }
508        }
509
510        work.validate_rename_targets(&targets, new_key)?;
511        for target in targets {
512            work.queue_key_rename(target.key, new_key)?;
513        }
514        work.commit_edits()?;
515        *self = work;
516        Ok(())
517    }
518
519    fn transaction(
520        &mut self,
521        operation: impl FnOnce(&mut YamlDoc) -> Result<(), YamlEditError>,
522    ) -> Result<(), YamlEditError> {
523        let mut work = self.clone();
524        operation(&mut work)?;
525        work.commit_edits()?;
526        *self = work;
527        Ok(())
528    }
529
530    fn resolve_rename_target(
531        &self,
532        document: usize,
533        pointer: &JsonPointer,
534    ) -> Result<RenameTarget, YamlEditError> {
535        let Some((parent_pointer, token)) = pointer.parent() else {
536            return Err(YamlEditError::new(
537                "a YAML document root does not have a mapping key to rename",
538            ));
539        };
540        let token_index = pointer.tokens().len().saturating_sub(1);
541        let mut parent = self.resolve_pointer(document, &parent_pointer)?;
542        parent = self.resolve_aliases_for_pointer(parent, pointer, token_index)?;
543        if !matches!(
544            self.semantic_kind(parent),
545            Some(SemanticKind::Mapping { .. })
546        ) {
547            return Err(YamlEditError::new(format!(
548                "JSON Pointer {:?} does not select a mapping member",
549                pointer.as_str()
550            )));
551        }
552        let matched = self
553            .mapping_match(parent, token, pointer, token_index)?
554            .ok_or_else(|| {
555                YamlEditError::new(format!(
556                    "mapping has no member {:?} to rename",
557                    token.as_str()
558                ))
559            })?;
560        Ok(RenameTarget {
561            mapping: parent,
562            key: matched.key,
563        })
564    }
565
566    fn validate_rename_targets(
567        &self,
568        targets: &[RenameTarget],
569        new_key: &str,
570    ) -> Result<(), YamlEditError> {
571        crate::validate_yaml_chars(new_key)?;
572        let mut targets_by_mapping = HashMap::<NodeId, HashSet<NodeId>>::new();
573        for target in targets {
574            self.validate_rename_key_node(target.key)?;
575            targets_by_mapping
576                .entry(target.mapping)
577                .or_default()
578                .insert(target.key);
579        }
580
581        for (mapping, renamed_keys) in targets_by_mapping {
582            let mut final_keys = HashSet::new();
583            for (key, _) in self.mapping_entries(mapping) {
584                let decoded = if renamed_keys.contains(&key) {
585                    new_key.to_owned()
586                } else {
587                    self.string_mapping_key(key)?
588                };
589                if !final_keys.insert(decoded.clone()) {
590                    return Err(YamlEditError::new(format!(
591                        "renaming a mapping key to {new_key:?} would create duplicate key {decoded:?}"
592                    )));
593                }
594            }
595        }
596        Ok(())
597    }
598
599    fn validate_rename_key_node(&self, key: NodeId) -> Result<(), YamlEditError> {
600        if self
601            .node(key)
602            .is_none_or(|node| node.kind() != crate::NodeKind::Scalar)
603            || !matches!(self.semantic_kind(key), Some(SemanticKind::Scalar { .. }))
604        {
605            return Err(YamlEditError::new(
606                "mapping-key rename supports only plain, single-quoted, and double-quoted string keys",
607            ));
608        }
609        if !scalar_is_string(self, key)? {
610            return Err(YamlEditError::new(
611                "mapping-key rename target is not a string scalar key",
612            ));
613        }
614        Ok(())
615    }
616
617    fn string_mapping_key(&self, key: NodeId) -> Result<String, YamlEditError> {
618        let mut resolved = key;
619        let mut seen = HashSet::new();
620        while matches!(self.semantic_kind(resolved), Some(SemanticKind::Alias)) {
621            if !seen.insert(resolved) {
622                let mut error = YamlEditError::new("cyclic YAML alias key");
623                if let Some(span) = self.node(resolved).map(|node| node.span()) {
624                    error = error.with_source_span(span);
625                }
626                return Err(error);
627            }
628            let alias = resolved;
629            resolved = self.resolve_alias(alias).ok_or_else(|| {
630                let mut error = YamlEditError::new("unresolved YAML alias key");
631                if let Some(span) = self.node(alias).map(|node| node.span()) {
632                    error = error.with_source_span(span);
633                }
634                error
635            })?;
636        }
637        if !scalar_is_string(self, resolved)? {
638            return Err(YamlEditError::new(
639                "affected mapping contains a non-string key",
640            ));
641        }
642        self.scalar_value(resolved)
643            .map(std::borrow::Cow::into_owned)
644            .map_err(Into::into)
645    }
646
647    fn queue_key_rename(&mut self, key: NodeId, new_key: &str) -> Result<(), YamlEditError> {
648        let current = self.scalar_value(key)?;
649        if current == new_key {
650            return Ok(());
651        }
652        let (span, style) = self.scalar_replacement_target(key)?;
653        let tag = self.resolved_tag(key)?;
654        let replacement = match style {
655            crate::ScalarStyle::Plain if safe_plain_key_with_tag(new_key, tag.as_deref()) => {
656                new_key.to_owned()
657            }
658            crate::ScalarStyle::Plain => crate::fragment::quote_string(new_key),
659            style => crate::format_scalar_value(new_key, style)
660                .unwrap_or_else(|_| crate::fragment::quote_string(new_key)),
661        };
662        self.queue_edit(span, replacement)?;
663        Ok(())
664    }
665
666    fn resolve_add_location(
667        &self,
668        document: usize,
669        pointer: &JsonPointer,
670    ) -> Result<AddLocation, YamlEditError> {
671        let Some((parent_pointer, token)) = pointer.parent() else {
672            let root = self
673                .document_root(document)?
674                .ok_or_else(|| YamlEditError::new("selected document has no root node"))?;
675            return Ok(AddLocation::Root(root));
676        };
677        let mut parent = self.resolve_pointer(document, &parent_pointer)?;
678        parent = self.resolve_aliases_for_pointer(
679            parent,
680            pointer,
681            pointer.tokens().len().saturating_sub(1),
682        )?;
683        match self.semantic_kind(parent) {
684            Some(SemanticKind::Mapping { .. }) => {
685                let existing = self
686                    .mapping_match(
687                        parent,
688                        token,
689                        pointer,
690                        pointer.tokens().len().saturating_sub(1),
691                    )?
692                    .map(|entry| entry.value);
693                Ok(AddLocation::Mapping {
694                    mapping: parent,
695                    existing,
696                    key: token.as_str().to_owned(),
697                })
698            }
699            Some(SemanticKind::Sequence { .. }) => {
700                let length = self.sequence_items(parent).count();
701                let parsed = parse_sequence_index(
702                    token,
703                    pointer,
704                    pointer.tokens().len().saturating_sub(1),
705                    true,
706                )?;
707                let index = if parsed == usize::MAX { length } else { parsed };
708                if index > length {
709                    return Err(YamlEditError::new(format!(
710                        "sequence index {index} is out of bounds for insertion into length {length}"
711                    )));
712                }
713                Ok(AddLocation::Sequence {
714                    sequence: parent,
715                    index,
716                })
717            }
718            _ => Err(YamlEditError::new(format!(
719                "add parent {:?} is not a mapping or sequence",
720                parent_pointer.as_str()
721            ))),
722        }
723    }
724
725    fn queue_add(
726        &mut self,
727        location: AddLocation,
728        value: &YamlFragment,
729    ) -> Result<(), YamlEditError> {
730        match location {
731            AddLocation::Root(root) => self.queue_fragment_replacement(root, value),
732            AddLocation::Mapping {
733                existing: Some(existing),
734                ..
735            } => self.queue_fragment_replacement(existing, value),
736            AddLocation::Mapping {
737                mapping,
738                existing: None,
739                key,
740            } => self.queue_mapping_insert(mapping, &key, value),
741            AddLocation::Sequence { sequence, index } => {
742                self.queue_sequence_insert(sequence, index, value)
743            }
744        }
745    }
746
747    pub(crate) fn queue_fragment_replacement(
748        &mut self,
749        target: NodeId,
750        value: &YamlFragment,
751    ) -> Result<(), YamlEditError> {
752        if self
753            .node(target)
754            .is_some_and(|node| node.kind() == crate::NodeKind::Scalar)
755            && matches!(
756                value.document().semantic_kind(value.root()),
757                Some(SemanticKind::Scalar { .. })
758            )
759            && value.document().raw_tag(value.root()).is_none()
760            && value.document().anchor(value.root()).is_none()
761        {
762            let mut replacement = value.to_yaml()?;
763            if !replacement.contains(['\n', '\r']) {
764                let (span, target_style) = self.scalar_replacement_target(target)?;
765                if scalar_is_string(self, target)?
766                    && scalar_is_string(value.document(), value.root())?
767                {
768                    let decoded = value.document().scalar_value(value.root())?;
769                    if let Ok(styled) = crate::format_scalar_value(&decoded, target_style) {
770                        replacement = styled;
771                    }
772                }
773                self.queue_edit(span, replacement)?;
774                return Ok(());
775            }
776        }
777        self.queue_fragment_replacement_whole(target, value)
778    }
779
780    pub(crate) fn queue_fragment_replacement_whole(
781        &mut self,
782        target: NodeId,
783        value: &YamlFragment,
784    ) -> Result<(), YamlEditError> {
785        let target_is_flow = matches!(
786            self.semantic_kind(target),
787            Some(
788                SemanticKind::Mapping {
789                    style: CollectionStyle::Flow
790                } | SemanticKind::Sequence {
791                    style: CollectionStyle::Flow
792                }
793            )
794        );
795        let replacement = if target_is_flow || self.is_flow_context(target) {
796            value.render_flow_for_replacement(self, target)?
797        } else {
798            let yaml = value.prepared_for_replacement(self, target)?.to_yaml()?;
799            indent_continuation_lines(&yaml, self.node_indent(self.expect_node(target)?))
800        };
801        self.replace_node_text(target, replacement)?;
802        Ok(())
803    }
804
805    pub(crate) fn queue_value_removal(&mut self, target: NodeId) -> Result<(), YamlEditError> {
806        let Some(entry) = self.containing_entry(target) else {
807            self.remove_node(target)?;
808            return Ok(());
809        };
810        let Some(collection) = self.node(entry).and_then(super::syntax::Node::parent) else {
811            self.remove_node(entry)?;
812            return Ok(());
813        };
814        let flow = matches!(
815            self.semantic_kind(collection),
816            Some(
817                SemanticKind::Mapping {
818                    style: CollectionStyle::Flow
819                } | SemanticKind::Sequence {
820                    style: CollectionStyle::Flow
821                }
822            )
823        );
824        if !flow {
825            let span = self.block_collection_entry_removal_span(collection, entry)?;
826            let empties_collection = self
827                .children(collection)
828                .filter(|node| self.containing_entry_child(*node))
829                .count()
830                == 1;
831            let empty = empties_collection
832                .then(|| match self.semantic_kind(collection) {
833                    Some(SemanticKind::Mapping { .. }) => Some("{}"),
834                    Some(SemanticKind::Sequence { .. }) => Some("[]"),
835                    _ => None,
836                })
837                .flatten()
838                .unwrap_or_default();
839            if empty.is_empty() {
840                self.queue_edit(span, String::new())?;
841            } else {
842                let (span, replacement) =
843                    self.empty_block_collection_edit(collection, span, empty)?;
844                self.queue_edit(span, replacement)?;
845            }
846            return Ok(());
847        }
848
849        let entries = self
850            .children(collection)
851            .filter(|node| self.containing_entry_child(*node))
852            .collect::<Vec<_>>();
853        let index = entries
854            .iter()
855            .position(|candidate| *candidate == entry)
856            .ok_or_else(|| YamlEditError::new("flow collection entry is missing"))?;
857        let entry_span = self.expect_node(entry)?.span;
858        if entries.get(index + 1).is_none() && self.flow_collection_is_multiline(collection)? {
859            if let Some(previous) = index.checked_sub(1).and_then(|index| entries.get(index)) {
860                let separator = self.flow_separator_after(*previous, entry)?;
861                self.queue_edit(separator, String::new())?;
862            }
863            let span = self.multiline_flow_terminal_entry_span(collection, entry)?;
864            self.queue_edit(span, String::new())?;
865            return Ok(());
866        }
867        let span = if let Some(next) = entries.get(index + 1).copied() {
868            Span::new(entry_span.start, self.expect_node(next)?.span.start)
869        } else if index > 0 {
870            let previous = self.expect_node(entries[index - 1])?.span;
871            Span::new(previous.end, entry_span.end)
872        } else {
873            entry_span
874        };
875        self.queue_edit(span, String::new())?;
876        Ok(())
877    }
878
879    fn flow_collection_is_multiline(&self, collection: NodeId) -> Result<bool, YamlEditError> {
880        Ok(self
881            .source()
882            .slice(self.expect_node(collection)?.span)
883            .contains(['\r', '\n']))
884    }
885
886    fn flow_entry_value_end(&self, entry: NodeId) -> Result<usize, YamlEditError> {
887        self.semantic_children(entry)
888            .last()
889            .and_then(|value| self.node(value))
890            .map(|value| value.span.end as usize)
891            .ok_or_else(|| YamlEditError::new("flow collection entry does not contain a value"))
892    }
893
894    fn flow_separator_after(&self, previous: NodeId, next: NodeId) -> Result<Span, YamlEditError> {
895        let start = self.flow_entry_value_end(previous)?;
896        let end = self.expect_node(next)?.span.start as usize;
897        let separator = self.source().as_str()[start..end]
898            .find(',')
899            .map(|relative| start + relative)
900            .ok_or_else(|| YamlEditError::new("flow collection entries have no separator"))?;
901        Ok(Span::from_usize(separator, separator + 1))
902    }
903
904    fn multiline_flow_terminal_entry_span(
905        &self,
906        collection: NodeId,
907        entry: NodeId,
908    ) -> Result<Span, YamlEditError> {
909        let collection_node = self.expect_node(collection)?;
910        let entry_node = self.expect_node(entry)?;
911        let delimiter = match collection_node.kind() {
912            crate::NodeKind::FlowMapping => '}',
913            crate::NodeKind::FlowSequence => ']',
914            _ => return Err(YamlEditError::new("expected a flow collection")),
915        };
916        let close = closing_delimiter_offset(self, collection_node.span, delimiter)?;
917        let content_end = self.flow_entry_value_end(entry)?;
918        let line_end = self.source().as_str()[content_end..close]
919            .find(['\r', '\n'])
920            .map(|relative| content_end + relative)
921            .unwrap_or(close);
922        let entry_line = self.line_start_offset(entry_node.span.start as usize);
923        let collection_line = self.line_start_offset(collection_node.span.start as usize);
924        if entry_line == collection_line || line_end == close {
925            return Ok(Span::from_usize(entry_node.span.start as usize, line_end));
926        }
927        let bytes = self.source().as_str().as_bytes();
928        let mut end = line_end;
929        if bytes.get(end) == Some(&b'\r') {
930            end += 1;
931            if bytes.get(end) == Some(&b'\n') {
932                end += 1;
933            }
934        } else if bytes.get(end) == Some(&b'\n') {
935            end += 1;
936        }
937        Ok(Span::from_usize(entry_line, end))
938    }
939
940    pub(crate) fn empty_block_collection_edit(
941        &self,
942        collection: NodeId,
943        removal_span: Span,
944        empty: &str,
945    ) -> Result<(Span, String), YamlEditError> {
946        let collection_node = self.expect_node(collection)?;
947        let removed = self.source().slice(removal_span);
948        let trailing_line_ending = if removed.ends_with("\r\n") {
949            "\r\n"
950        } else if removed.ends_with('\n') {
951            "\n"
952        } else if removed.ends_with('\r') {
953            "\r"
954        } else {
955            ""
956        };
957        let empty_with_break = format!("{empty}{trailing_line_ending}");
958        let value_start = self
959            .children(collection)
960            .find(|child| self.containing_entry_child(*child))
961            .map(|entry| {
962                self.expect_node(entry).map(|entry| {
963                    usize::max(
964                        entry.span.start as usize,
965                        collection_node.span.start as usize,
966                    )
967                })
968            })
969            .transpose()?
970            .unwrap_or(self.node_value_start(collection)?);
971        let Some(entry) = collection_node.parent() else {
972            return Ok((
973                Span::from_usize(value_start, removal_span.end as usize),
974                empty_with_break,
975            ));
976        };
977        let entry_node = self.expect_node(entry)?;
978        if !matches!(
979            entry_node.kind(),
980            crate::NodeKind::MappingEntry | crate::NodeKind::SequenceEntry
981        ) || self.line_start_offset(value_start)
982            == self.line_start_offset(entry_node.span.start as usize)
983        {
984            return Ok((
985                Span::from_usize(value_start, removal_span.end as usize),
986                empty_with_break,
987            ));
988        }
989
990        let prefix_span = Span::from_usize(entry_node.span.start as usize, value_start);
991        let prefix = self.source().slice(prefix_span);
992        let line_break = prefix.find(['\r', '\n']).ok_or_else(|| {
993            YamlEditError::new("multiline block collection has no preceding line break")
994        })?;
995        let line_break_end = if prefix.as_bytes().get(line_break) == Some(&b'\r')
996            && prefix.as_bytes().get(line_break + 1) == Some(&b'\n')
997        {
998            line_break + 2
999        } else {
1000            line_break + 1
1001        };
1002        let head = &prefix[..line_break];
1003        let uncommented = strip_inline_comment(head);
1004        let content = uncommented.trim_end_matches([' ', '\t']);
1005        let comment = &head[uncommented.len()..];
1006        let between = prefix[line_break_end..].trim_end_matches([' ', '\t']);
1007        let mut replacement = format!("{content} {empty}");
1008        if !comment.is_empty() {
1009            replacement.push(' ');
1010            replacement.push_str(comment.trim_start());
1011        }
1012        if !between.is_empty() {
1013            replacement.push_str(&prefix[line_break..line_break_end]);
1014            replacement.push_str(between);
1015        }
1016        replacement.push_str(trailing_line_ending);
1017        Ok((
1018            Span::new(entry_node.span.start, removal_span.end),
1019            replacement,
1020        ))
1021    }
1022
1023    pub(crate) fn containing_entry_child(&self, node: NodeId) -> bool {
1024        self.node(node).is_some_and(|node| {
1025            matches!(
1026                node.kind(),
1027                crate::NodeKind::MappingEntry | crate::NodeKind::SequenceEntry
1028            )
1029        })
1030    }
1031
1032    pub(crate) fn queue_mapping_insert(
1033        &mut self,
1034        mapping: NodeId,
1035        key: &str,
1036        value: &YamlFragment,
1037    ) -> Result<(), YamlEditError> {
1038        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
1039            return Err(YamlEditError::new(
1040                "mapping insertion target is not a mapping",
1041            ));
1042        };
1043        let key = emit_string_key(key);
1044        match style {
1045            CollectionStyle::Flow => {
1046                let mapping_node = self.expect_node(mapping)?;
1047                let close = closing_delimiter_offset(self, mapping_node.span, '}')?;
1048                let entries = self
1049                    .children(mapping)
1050                    .filter(|node| self.containing_entry_child(*node))
1051                    .collect::<Vec<_>>();
1052                let value = value.render_flow(self)?;
1053                if self.queue_multiline_flow_append(
1054                    mapping,
1055                    close,
1056                    &entries,
1057                    &format!("{key}: {value}"),
1058                )? {
1059                    return Ok(());
1060                }
1061                let has_pending_entry = self.edits.iter().any(|edit| {
1062                    edit.span == Span::empty_from_usize(close) && !edit.replacement.is_empty()
1063                });
1064                let prefix = if self.mapping_entries(mapping).next().is_some() || has_pending_entry
1065                {
1066                    ", "
1067                } else {
1068                    ""
1069                };
1070                self.queue_edit(
1071                    Span::empty_from_usize(close),
1072                    format!("{prefix}{key}: {value}"),
1073                )?;
1074            }
1075            CollectionStyle::Block => {
1076                let mapping_node = self.expect_node(mapping)?;
1077                let indent = self.block_mapping_entry_indent(mapping);
1078                let offset = self.mapping_insertion_offset(mapping_node);
1079                let preserve_unterminated_eof = insertion_at_unterminated_eof(self, offset);
1080                let mut insertion = insertion_prefix(self, offset);
1081                let value = value.prepared(self)?.to_yaml()?;
1082                let line_ending = self.preferred_line_ending().to_owned();
1083                insertion.push_str(&format_block_mapping_entry(
1084                    &key,
1085                    &value,
1086                    indent,
1087                    &line_ending,
1088                ));
1089                if preserve_unterminated_eof && !value.contains(['\n', '\r']) {
1090                    insertion.truncate(insertion.len() - line_ending.len());
1091                }
1092                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
1093            }
1094        }
1095        Ok(())
1096    }
1097
1098    pub(crate) fn queue_mapping_insert_before(
1099        &mut self,
1100        mapping: NodeId,
1101        before_entry: NodeId,
1102        key: &str,
1103        value: &YamlFragment,
1104    ) -> Result<(), YamlEditError> {
1105        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
1106            return Err(YamlEditError::new(
1107                "mapping insertion target is not a mapping",
1108            ));
1109        };
1110        if style != CollectionStyle::Flow {
1111            return Err(YamlEditError::new(
1112                "ordered flow insertion requires a flow mapping",
1113            ));
1114        }
1115        let entry = self.expect_node(before_entry)?;
1116        let key = emit_string_key(key);
1117        let value = value.render_flow(self)?;
1118        if self.flow_collection_is_multiline(mapping)? {
1119            let content_start = self.flow_entry_content_start(before_entry)?;
1120            let line_start = self.line_start_offset(content_start);
1121            if line_start != self.line_start_offset(self.expect_node(mapping)?.span.start as usize)
1122            {
1123                let indent = &self.source().as_str()[line_start..content_start];
1124                self.queue_edit(
1125                    Span::empty_from_usize(line_start),
1126                    format!("{indent}{key}: {value},{}", self.preferred_line_ending()),
1127                )?;
1128                return Ok(());
1129            }
1130        }
1131        self.queue_edit(Span::empty(entry.span.start), format!("{key}: {value}, "))?;
1132        Ok(())
1133    }
1134
1135    pub(crate) fn queue_sequence_insert(
1136        &mut self,
1137        sequence: NodeId,
1138        index: usize,
1139        value: &YamlFragment,
1140    ) -> Result<(), YamlEditError> {
1141        let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
1142            return Err(YamlEditError::new(
1143                "sequence insertion target is not a sequence",
1144            ));
1145        };
1146        let items = self.sequence_items(sequence).collect::<Vec<_>>();
1147        match style {
1148            CollectionStyle::Flow => {
1149                let sequence_span = self.expect_node(sequence)?.span;
1150                let entries = self
1151                    .children(sequence)
1152                    .filter(|node| self.containing_entry_child(*node))
1153                    .collect::<Vec<_>>();
1154                let close = closing_delimiter_offset(self, sequence_span, ']')?;
1155                let value = value.render_flow(self)?;
1156                if index == items.len()
1157                    && self.queue_multiline_flow_append(sequence, close, &entries, &value)?
1158                {
1159                    return Ok(());
1160                }
1161                if let Some(entry) = entries.get(index).copied()
1162                    && self.flow_collection_is_multiline(sequence)?
1163                {
1164                    let content_start = self.flow_entry_content_start(entry)?;
1165                    let line_start = self.line_start_offset(content_start);
1166                    if line_start != self.line_start_offset(sequence_span.start as usize) {
1167                        let indent = &self.source().as_str()[line_start..content_start];
1168                        self.queue_edit(
1169                            Span::empty_from_usize(line_start),
1170                            format!("{indent}{value},{}", self.preferred_line_ending()),
1171                        )?;
1172                        return Ok(());
1173                    }
1174                }
1175                let offset = if let Some(item) = items.get(index).copied() {
1176                    self.expect_node(item)?.span.start as usize
1177                } else {
1178                    close
1179                };
1180                let insertion = if items.is_empty() {
1181                    value
1182                } else if index < items.len() {
1183                    format!("{value}, ")
1184                } else {
1185                    format!(", {value}")
1186                };
1187                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
1188            }
1189            CollectionStyle::Block => {
1190                let sequence_node = self.expect_node(sequence)?;
1191                let indent = self.node_indent(sequence_node);
1192                let offset = if let Some(item) = items.get(index).copied() {
1193                    let entry = self.containing_entry(item).unwrap_or(item);
1194                    self.line_start_offset(self.expect_node(entry)?.span.start as usize)
1195                } else {
1196                    self.sequence_insertion_offset(sequence_node)
1197                };
1198                let preserve_unterminated_eof = insertion_at_unterminated_eof(self, offset);
1199                let mut insertion = insertion_prefix(self, offset);
1200                let value = value.prepared(self)?.to_yaml()?;
1201                let line_ending = self.preferred_line_ending().to_owned();
1202                insertion.push_str(&format_block_sequence_entry(&value, indent, &line_ending));
1203                if preserve_unterminated_eof && !value.contains(['\n', '\r']) {
1204                    insertion.truncate(insertion.len() - line_ending.len());
1205                }
1206                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
1207            }
1208        }
1209        Ok(())
1210    }
1211
1212    fn flow_entry_content_start(&self, entry: NodeId) -> Result<usize, YamlEditError> {
1213        self.semantic_children(entry)
1214            .next()
1215            .and_then(|value| self.node(value))
1216            .map(|value| value.span.start as usize)
1217            .ok_or_else(|| YamlEditError::new("flow collection entry does not contain a value"))
1218    }
1219
1220    fn queue_multiline_flow_append(
1221        &mut self,
1222        collection: NodeId,
1223        close: usize,
1224        entries: &[NodeId],
1225        rendered: &str,
1226    ) -> Result<bool, YamlEditError> {
1227        if !self.flow_collection_is_multiline(collection)? {
1228            return Ok(false);
1229        }
1230        let close_line = self.line_start_offset(close);
1231        let indent = if let Some(last) = entries.last().copied() {
1232            let content_start = self.flow_entry_content_start(last)?;
1233            if self.line_start_offset(content_start) == close_line {
1234                return Ok(false);
1235            }
1236            self.source().as_str()[self.line_start_offset(content_start)..content_start].to_owned()
1237        } else {
1238            let close_indent = self.source().as_str()[close_line..close]
1239                .bytes()
1240                .take_while(|byte| *byte == b' ')
1241                .count();
1242            " ".repeat(close_indent + 2)
1243        };
1244        if let Some(last) = entries.last().copied() {
1245            let separator = Span::empty_from_usize(self.flow_entry_value_end(last)?);
1246            self.queue_edit(separator, ",".to_owned())?;
1247        }
1248        self.queue_edit(
1249            Span::empty_from_usize(close_line),
1250            format!("{indent}{rendered}{}", self.preferred_line_ending()),
1251        )?;
1252        Ok(true)
1253    }
1254
1255    pub(crate) fn is_flow_context(&self, mut node: NodeId) -> bool {
1256        while let Some(parent) = self.node(node).and_then(super::syntax::Node::parent) {
1257            if matches!(
1258                self.semantic_kind(parent),
1259                Some(
1260                    SemanticKind::Mapping {
1261                        style: CollectionStyle::Flow
1262                    } | SemanticKind::Sequence {
1263                        style: CollectionStyle::Flow
1264                    }
1265                )
1266            ) {
1267                return true;
1268            }
1269            node = parent;
1270        }
1271        false
1272    }
1273
1274    fn anchor_has_external_alias(&self, root: NodeId) -> bool {
1275        let Some(root_span) = self.node(root).map(super::syntax::Node::span) else {
1276            return false;
1277        };
1278        let anchored = self
1279            .nodes
1280            .iter()
1281            .enumerate()
1282            .map(|(index, _)| NodeId::from_usize(index))
1283            .filter(|node| {
1284                self.anchor(*node).is_some()
1285                    && self.node(*node).is_some_and(|node| {
1286                        node.span().start >= root_span.start && node.span().end <= root_span.end
1287                    })
1288            })
1289            .collect::<Vec<_>>();
1290        self.nodes
1291            .iter()
1292            .enumerate()
1293            .map(|(index, _)| NodeId::from_usize(index))
1294            .filter(|node| matches!(self.semantic_kind(*node), Some(SemanticKind::Alias)))
1295            .any(|alias| {
1296                let outside = self.node(alias).is_some_and(|node| {
1297                    node.span().start < root_span.start || node.span().end > root_span.end
1298                });
1299                outside
1300                    && self
1301                        .resolve_alias(alias)
1302                        .is_some_and(|target| anchored.contains(&target))
1303            })
1304    }
1305
1306    pub(crate) fn line_start_offset(&self, offset: usize) -> usize {
1307        let offset = u32::try_from(offset).unwrap_or(u32::MAX);
1308        match self.source.line_starts().binary_search(&offset) {
1309            Ok(index) => self.source.line_starts()[index] as usize,
1310            Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
1311        }
1312    }
1313}
1314
1315pub(crate) fn closing_delimiter_offset(
1316    doc: &YamlDoc,
1317    span: Span,
1318    delimiter: char,
1319) -> Result<usize, YamlEditError> {
1320    let source = doc.source.slice(span);
1321    let relative = source
1322        .rfind(delimiter)
1323        .ok_or_else(|| YamlEditError::new(format!("missing `{delimiter}` delimiter")))?;
1324    Ok(span.start as usize + relative)
1325}
1326
1327fn insertion_prefix(doc: &YamlDoc, offset: usize) -> String {
1328    if insertion_at_unterminated_eof(doc, offset) {
1329        doc.preferred_line_ending().to_owned()
1330    } else {
1331        String::new()
1332    }
1333}
1334
1335fn insertion_at_unterminated_eof(doc: &YamlDoc, offset: usize) -> bool {
1336    offset == doc.source.len()
1337        && !doc
1338            .source
1339            .as_str()
1340            .as_bytes()
1341            .last()
1342            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
1343}
1344
1345fn format_block_mapping_entry(key: &str, value: &str, indent: usize, ending: &str) -> String {
1346    let prefix = " ".repeat(indent);
1347    if !value.contains(['\n', '\r']) {
1348        return format!("{prefix}{key}: {value}{ending}");
1349    }
1350    let value = indent_text(value, indent + 2);
1351    let mut output = format!("{prefix}{key}:{ending}{value}");
1352    if !output.ends_with(['\n', '\r']) {
1353        output.push_str(ending);
1354    }
1355    output
1356}
1357
1358fn format_block_sequence_entry(value: &str, indent: usize, ending: &str) -> String {
1359    let prefix = " ".repeat(indent);
1360    if !value.contains(['\n', '\r']) {
1361        return format!("{prefix}- {value}{ending}");
1362    }
1363    let value = indent_text(value, indent + 2);
1364    let mut output = format!("{prefix}-{ending}{value}");
1365    if !output.ends_with(['\n', '\r']) {
1366        output.push_str(ending);
1367    }
1368    output
1369}
1370
1371fn indent_continuation_lines(value: &str, indent: usize) -> String {
1372    if indent == 0 {
1373        return value.to_owned();
1374    }
1375    let prefix = " ".repeat(indent);
1376    let mut output = String::with_capacity(value.len());
1377    let mut after_break = false;
1378    for character in value.chars() {
1379        if after_break && !matches!(character, '\r' | '\n') {
1380            output.push_str(&prefix);
1381            after_break = false;
1382        }
1383        output.push(character);
1384        if character == '\n' {
1385            after_break = true;
1386        } else if character != '\r' {
1387            after_break = false;
1388        }
1389    }
1390    output
1391}
1392
1393pub(crate) fn emit_string_key(value: &str) -> String {
1394    if safe_plain_string(value) {
1395        value.to_owned()
1396    } else {
1397        crate::fragment::quote_string(value)
1398    }
1399}
1400
1401pub(crate) fn safe_plain_string(value: &str) -> bool {
1402    if !safe_plain_string_syntax(value) {
1403        return false;
1404    }
1405    matches!(
1406        resolve_scalar(value, YamlScalarStyle::Plain, None),
1407        Ok(ResolvedScalar::String)
1408    )
1409}
1410
1411fn safe_plain_string_syntax(value: &str) -> bool {
1412    if value.is_empty()
1413        || value.trim() != value
1414        || value.contains(['\n', '\r', '\t', ':', '#', '[', ']', '{', '}', ','])
1415        || value.starts_with(['-', '?', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'])
1416    {
1417        return false;
1418    }
1419    true
1420}
1421
1422fn safe_plain_key_with_tag(value: &str, tag: Option<&str>) -> bool {
1423    const STRING_TAG: &str = "tag:yaml.org,2002:str";
1424
1425    safe_plain_string_syntax(value)
1426        && (tag == Some(STRING_TAG)
1427            || matches!(
1428                resolve_scalar(value, YamlScalarStyle::Plain, None),
1429                Ok(ResolvedScalar::String)
1430            ))
1431}
1432
1433fn scalar_is_string(doc: &YamlDoc, node: NodeId) -> Result<bool, YamlEditError> {
1434    let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(node) else {
1435        return Ok(false);
1436    };
1437    let value = doc.scalar_value(node)?;
1438    let tag = doc.resolved_tag(node)?;
1439    Ok(matches!(
1440        resolve_scalar(&value, style, tag.as_deref()),
1441        Ok(ResolvedScalar::String)
1442    ))
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447    use super::*;
1448
1449    fn pointer(value: &str) -> JsonPointer {
1450        JsonPointer::parse(value).unwrap()
1451    }
1452
1453    fn fragment(value: &str) -> YamlFragment {
1454        YamlFragment::parse(value).unwrap()
1455    }
1456
1457    #[test]
1458    fn renames_block_flow_and_explicit_mapping_keys_losslessly() {
1459        let input = "old: 1 # keep\nflow: {old: 2}\nexplicit:\n  ? 'old'\n  : 3\n";
1460        let mut doc = YamlDoc::parse(input).unwrap();
1461        doc.rename_keys_at(
1462            0,
1463            &[
1464                pointer("/old"),
1465                pointer("/flow/old"),
1466                pointer("/explicit/old"),
1467            ],
1468            "true",
1469        )
1470        .unwrap();
1471
1472        assert_eq!(
1473            doc.as_source(),
1474            "\"true\": 1 # keep\nflow: {\"true\": 2}\nexplicit:\n  ? 'true'\n  : 3\n"
1475        );
1476    }
1477
1478    #[test]
1479    fn rename_key_quotes_plain_names_that_are_not_safe_strings() {
1480        for new_key in ["true", "", "a: b", "line\nbreak"] {
1481            let mut doc = YamlDoc::parse("old: value\n").unwrap();
1482            doc.rename_key_at(0, &pointer("/old"), new_key).unwrap();
1483            assert_eq!(
1484                doc.as_source(),
1485                format!("{}: value\n", crate::fragment::quote_string(new_key))
1486            );
1487        }
1488
1489        let mut flow = YamlDoc::parse("{old: value}\n").unwrap();
1490        flow.rename_key_at(0, &pointer("/old"), "a,b").unwrap();
1491        assert_eq!(flow.as_source(), "{\"a,b\": value}\n");
1492    }
1493
1494    #[test]
1495    fn rename_key_preserves_quoted_styles_properties_comments_and_line_endings() {
1496        let mut single = YamlDoc::parse("'old': value\n").unwrap();
1497        single.rename_key_at(0, &pointer("/old"), "Bob's").unwrap();
1498        assert_eq!(single.as_source(), "'Bob''s': value\n");
1499
1500        let mut double = YamlDoc::parse("\"old\": value\n").unwrap();
1501        double
1502            .rename_key_at(0, &pointer("/old"), "new \"key\"")
1503            .unwrap();
1504        assert_eq!(double.as_source(), "\"new \\\"key\\\"\": value\n");
1505
1506        let mut tagged = YamlDoc::parse("!!str &key old: value # keep\r\n").unwrap();
1507        tagged.rename_key_at(0, &pointer("/old"), "true").unwrap();
1508        assert_eq!(tagged.as_source(), "!!str &key true: value # keep\r\n");
1509    }
1510
1511    #[test]
1512    fn rename_keys_resolves_all_targets_before_editing_and_deduplicates_alias_routes() {
1513        let mut nested = YamlDoc::parse("parent:\n  old: 1\nold: 2\n").unwrap();
1514        nested
1515            .rename_keys_at(0, &[pointer("/parent/old"), pointer("/parent")], "renamed")
1516            .unwrap();
1517        assert_eq!(nested.as_source(), "renamed:\n  renamed: 1\nold: 2\n");
1518
1519        let mut aliases = YamlDoc::parse("base: &base\n  name: Ada\ncopy: *base\n").unwrap();
1520        aliases
1521            .rename_keys_at(
1522                0,
1523                &[pointer("/base/name"), pointer("/copy/name")],
1524                "display-name",
1525            )
1526            .unwrap();
1527        assert_eq!(
1528            aliases.as_source(),
1529            "base: &base\n  display-name: Ada\ncopy: *base\n"
1530        );
1531    }
1532
1533    #[test]
1534    fn rename_keys_rejects_collisions_transactionally() {
1535        let input = "a: 1\nb: 2\n";
1536        let mut existing = YamlDoc::parse(input).unwrap();
1537        let error = existing.rename_key_at(0, &pointer("/a"), "b").unwrap_err();
1538        assert!(error.to_string().contains("duplicate key \"b\""));
1539        assert_eq!(existing.as_source(), input);
1540
1541        let mut selected = YamlDoc::parse(input).unwrap();
1542        let error = selected
1543            .rename_keys_at(0, &[pointer("/a"), pointer("/b")], "x")
1544            .unwrap_err();
1545        assert!(error.to_string().contains("duplicate key \"x\""));
1546        assert_eq!(selected.as_source(), input);
1547    }
1548
1549    #[test]
1550    fn rename_keys_treats_duplicate_and_unchanged_targets_as_no_ops() {
1551        let input = "\"old\": value\n";
1552        let mut doc = YamlDoc::parse(input).unwrap();
1553        doc.rename_keys_at(0, &[pointer("/old"), pointer("/old")], "old")
1554            .unwrap();
1555        doc.rename_keys_at(0, &[], "ignored").unwrap();
1556        assert_eq!(doc.as_source(), input);
1557    }
1558
1559    #[test]
1560    fn rename_key_rejects_non_members_and_unsupported_key_forms() {
1561        let mut root = YamlDoc::parse("key: value\n").unwrap();
1562        assert!(
1563            root.rename_key_at(0, &pointer(""), "new")
1564                .unwrap_err()
1565                .to_string()
1566                .contains("document root")
1567        );
1568        assert_eq!(root.as_source(), "key: value\n");
1569
1570        let mut sequence = YamlDoc::parse("- value\n").unwrap();
1571        assert!(
1572            sequence
1573                .rename_key_at(0, &pointer("/0"), "new")
1574                .unwrap_err()
1575                .to_string()
1576                .contains("does not select a mapping member")
1577        );
1578
1579        let mut block = YamlDoc::parse("? >\n  old\n: value\n").unwrap();
1580        assert!(
1581            block
1582                .rename_key_at(0, &pointer("/old\n"), "new")
1583                .unwrap_err()
1584                .to_string()
1585                .contains("plain, single-quoted, and double-quoted")
1586        );
1587
1588        let mut alias = YamlDoc::parse("name: &key target\n? *key\n: value\n").unwrap();
1589        let error = alias
1590            .rename_key_at(0, &pointer("/target"), "new")
1591            .unwrap_err();
1592        assert!(
1593            error
1594                .to_string()
1595                .contains("plain, single-quoted, and double-quoted"),
1596            "{error}"
1597        );
1598
1599        let mut complex = YamlDoc::parse("? [a, b]\n: value\n").unwrap();
1600        assert!(
1601            complex
1602                .rename_key_at(0, &pointer("/anything"), "new")
1603                .is_err()
1604        );
1605    }
1606
1607    #[test]
1608    fn adds_replaces_and_removes_block_mapping_values() {
1609        let mut doc = YamlDoc::parse("server:\n  host: localhost # keep\n").unwrap();
1610        doc.add_at(0, &pointer("/server/port"), &fragment("8080"))
1611            .unwrap();
1612        assert_eq!(
1613            doc.as_source(),
1614            "server:\n  host: localhost # keep\n  port: 8080\n"
1615        );
1616        doc.replace_at(0, &pointer("/server/host"), &fragment("example.com"))
1617            .unwrap();
1618        assert_eq!(
1619            doc.as_source(),
1620            "server:\n  host: example.com # keep\n  port: 8080\n"
1621        );
1622        doc.remove_at(0, &pointer("/server/port")).unwrap();
1623        assert_eq!(doc.as_source(), "server:\n  host: example.com # keep\n");
1624    }
1625
1626    #[test]
1627    fn replaces_compact_and_expanded_block_sequences_with_flow_fragments() {
1628        for input in [
1629            "releases:\n  - artifact: client\n    channel: stable\ntail: retained\n",
1630            "releases:\n  -\n    artifact: client\n    channel: stable\ntail: retained\n",
1631        ] {
1632            let mut doc = YamlDoc::parse(input).unwrap();
1633            doc.replace_at(
1634                0,
1635                &pointer("/releases"),
1636                &fragment("[{artifact: server, channel: preview}]"),
1637            )
1638            .unwrap();
1639
1640            assert_eq!(
1641                doc.as_source(),
1642                "releases:\n  [{artifact: server, channel: preview}]\ntail: retained\n"
1643            );
1644            let reparsed = YamlDoc::parse(doc.as_source()).unwrap();
1645            let artifact = reparsed
1646                .resolve_pointer(0, &pointer("/releases/0/artifact"))
1647                .unwrap();
1648            let channel = reparsed
1649                .resolve_pointer(0, &pointer("/releases/0/channel"))
1650                .unwrap();
1651            assert_eq!(reparsed.scalar_value(artifact).unwrap(), "server");
1652            assert_eq!(reparsed.scalar_value(channel).unwrap(), "preview");
1653        }
1654    }
1655
1656    #[test]
1657    fn collection_replacement_keeps_the_owning_entry_comment() {
1658        let mut doc = YamlDoc::parse(
1659            "jobs: # scheduling note\n  - # obsolete job note\n    command: build\n    retries: 2\nafter: unchanged\n",
1660        )
1661        .unwrap();
1662
1663        doc.replace_at(
1664            0,
1665            &pointer("/jobs"),
1666            &fragment("[{command: test, retries: 1}]"),
1667        )
1668        .unwrap();
1669
1670        assert_eq!(
1671            doc.as_source(),
1672            "jobs: # scheduling note\n  [{command: test, retries: 1}]\nafter: unchanged\n"
1673        );
1674        doc.commit_edits().unwrap();
1675    }
1676
1677    #[test]
1678    fn inserts_into_compact_sequence_entry_mappings_at_the_key_column() {
1679        let input = "services:\n  - name: api\n    port: 8080\n";
1680
1681        let mut scalar = YamlDoc::parse(input).unwrap();
1682        scalar
1683            .add_at(0, &pointer("/services/0/enabled"), &fragment("true"))
1684            .unwrap();
1685        assert_eq!(
1686            scalar.as_source(),
1687            "services:\n  - name: api\n    port: 8080\n    enabled: true\n"
1688        );
1689        scalar.commit_edits().unwrap();
1690
1691        let mut nested = YamlDoc::parse(input).unwrap();
1692        nested
1693            .add_at(
1694                0,
1695                &pointer("/services/0/tls"),
1696                &fragment("{enabled: true, mode: strict}"),
1697            )
1698            .unwrap();
1699        assert_eq!(
1700            nested.as_source(),
1701            "services:\n  - name: api\n    port: 8080\n    tls: {enabled: true, mode: strict}\n"
1702        );
1703        nested.commit_edits().unwrap();
1704    }
1705
1706    #[test]
1707    fn adds_string_keys_without_changing_their_schema_type() {
1708        let mut doc = YamlDoc::parse("{}\n").unwrap();
1709        doc.add_at(0, &pointer("/true"), &fragment("value"))
1710            .unwrap();
1711        assert_eq!(doc.as_source(), "{\"true\": value}\n");
1712        assert!(
1713            doc.resolve_pointer(0, &pointer("/true")).is_ok(),
1714            "{}",
1715            doc.as_source()
1716        );
1717    }
1718
1719    #[test]
1720    fn inserts_block_and_flow_sequence_items() {
1721        let mut block = YamlDoc::parse("items:\n  - a\n  - c\n").unwrap();
1722        block
1723            .add_at(0, &pointer("/items/1"), &fragment("b"))
1724            .unwrap();
1725        block
1726            .add_at(0, &pointer("/items/-"), &fragment("d"))
1727            .unwrap();
1728        assert_eq!(block.as_source(), "items:\n  - a\n  - b\n  - c\n  - d\n");
1729
1730        let mut flow = YamlDoc::parse("items: [a, c]\n").unwrap();
1731        flow.add_at(0, &pointer("/items/1"), &fragment("b"))
1732            .unwrap();
1733        assert_eq!(flow.as_source(), "items: [a, b, c]\n");
1734    }
1735
1736    #[test]
1737    fn sequence_editor_retains_block_and_flow_items_with_trivia() {
1738        let mut block = YamlDoc::parse(
1739            "items:\n  # first\n  - keep # inline\n  # second\n  - remove\n  - last\n",
1740        )
1741        .unwrap();
1742        let sequence = block.resolve_pointer(0, &pointer("/items")).unwrap();
1743        block
1744            .sequence_editor(sequence)
1745            .unwrap()
1746            .retain(|doc, item| doc.scalar_value(item).unwrap() != "remove")
1747            .unwrap();
1748        assert_eq!(
1749            block.as_source(),
1750            "items:\n  # first\n  - keep # inline\n  - last\n"
1751        );
1752
1753        let mut flow = YamlDoc::parse("items: [one, two, three]\n").unwrap();
1754        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1755        flow.sequence_editor(sequence)
1756            .unwrap()
1757            .retain(|doc, item| doc.scalar_value(item).unwrap() != "two")
1758            .unwrap();
1759        assert_eq!(flow.as_source(), "items: [one, three]\n");
1760
1761        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1762        flow.sequence_editor(sequence)
1763            .unwrap()
1764            .retain(|_, _| false)
1765            .unwrap();
1766        assert_eq!(flow.as_source(), "items: []\n");
1767    }
1768
1769    #[test]
1770    fn sequence_editor_inserts_fragments_without_changing_existing_items() {
1771        let mut block = YamlDoc::parse("items:\r\n  - one # keep\r\n  - three\r\n").unwrap();
1772        let sequence = block.resolve_pointer(0, &pointer("/items")).unwrap();
1773        block
1774            .sequence_editor(sequence)
1775            .unwrap()
1776            .insert(1, &fragment("{name: two}"))
1777            .unwrap();
1778        assert_eq!(
1779            block.as_source(),
1780            "items:\r\n  - one # keep\r\n  - {name: two}\r\n  - three\r\n"
1781        );
1782
1783        let mut flow = YamlDoc::parse("items: [\n  one,\n  three\n]\n").unwrap();
1784        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1785        flow.sequence_editor(sequence)
1786            .unwrap()
1787            .insert(1, &fragment("two"))
1788            .unwrap();
1789        assert_eq!(flow.as_source(), "items: [\n  one,\n  two,\n  three\n]\n");
1790
1791        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1792        let before = flow.as_source().to_owned();
1793        assert!(
1794            flow.sequence_editor(sequence)
1795                .unwrap()
1796                .insert(4, &fragment("nope"))
1797                .is_err()
1798        );
1799        assert_eq!(flow.as_source(), before);
1800    }
1801
1802    #[test]
1803    fn sequence_editor_moves_complete_items_with_their_trivia() {
1804        let mut block = YamlDoc::parse(
1805            "items:\n  # alpha\n  - name: a\n    value: 1\n  # beta\n  - name: b # inline\n  - name: c\n",
1806        )
1807        .unwrap();
1808        let sequence = block.resolve_pointer(0, &pointer("/items")).unwrap();
1809        block
1810            .sequence_editor(sequence)
1811            .unwrap()
1812            .move_item(1, 0)
1813            .unwrap();
1814        assert_eq!(
1815            block.as_source(),
1816            "items:\n  # beta\n  - name: b # inline\n  # alpha\n  - name: a\n    value: 1\n  - name: c\n"
1817        );
1818
1819        let mut flow = YamlDoc::parse("items: [a, # beta\n  b, c]\n").unwrap();
1820        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1821        flow.sequence_editor(sequence)
1822            .unwrap()
1823            .move_item(1, 2)
1824            .unwrap();
1825        assert_eq!(flow.as_source(), "items: [a, c, # beta\n  b]\n");
1826
1827        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1828        let before = flow.as_source().to_owned();
1829        flow.sequence_editor(sequence)
1830            .unwrap()
1831            .move_item(1, 1)
1832            .unwrap();
1833        assert_eq!(flow.as_source(), before);
1834    }
1835
1836    #[test]
1837    fn sequence_editor_stably_sorts_complete_items() {
1838        let mut block = YamlDoc::parse(
1839            "items:\r\n  # second first\r\n  - b\r\n  # first\r\n  - a\r\n  # second duplicate\r\n  - b\r\n",
1840        )
1841        .unwrap();
1842        let sequence = block.resolve_pointer(0, &pointer("/items")).unwrap();
1843        block
1844            .sequence_editor(sequence)
1845            .unwrap()
1846            .sort_by(|doc, left, right| {
1847                doc.scalar_value(left)
1848                    .unwrap()
1849                    .cmp(&doc.scalar_value(right).unwrap())
1850            })
1851            .unwrap();
1852        assert_eq!(
1853            block.as_source(),
1854            "items:\r\n  # first\r\n  - a\r\n  # second first\r\n  - b\r\n  # second duplicate\r\n  - b\r\n"
1855        );
1856
1857        let mut flow = YamlDoc::parse("items: [c, # a\n  a, b]\n").unwrap();
1858        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1859        flow.sequence_editor(sequence)
1860            .unwrap()
1861            .sort_by(|doc, left, right| {
1862                doc.scalar_value(left)
1863                    .unwrap()
1864                    .cmp(&doc.scalar_value(right).unwrap())
1865            })
1866            .unwrap();
1867        assert_eq!(flow.as_source(), "items: [ # a\n  a, b,c]\n");
1868
1869        let sequence = flow.resolve_pointer(0, &pointer("/items")).unwrap();
1870        let before = flow.as_source().to_owned();
1871        flow.sequence_editor(sequence)
1872            .unwrap()
1873            .sort_by(|doc, left, right| {
1874                doc.scalar_value(left)
1875                    .unwrap()
1876                    .cmp(&doc.scalar_value(right).unwrap())
1877            })
1878            .unwrap();
1879        assert_eq!(flow.as_source(), before);
1880    }
1881
1882    #[test]
1883    fn inserts_into_multiline_flow_collections_without_collapsing_layout() {
1884        let mut mapping = YamlDoc::parse("map: {\n  a: 1 # keep\n}\ntail: keep\n").unwrap();
1885        mapping
1886            .add_at(0, &pointer("/map/b"), &fragment("2"))
1887            .unwrap();
1888        assert_eq!(
1889            mapping.as_source(),
1890            "map: {\n  a: 1, # keep\n  b: 2\n}\ntail: keep\n"
1891        );
1892        mapping.commit_edits().unwrap();
1893
1894        let mut empty_mapping = YamlDoc::parse("outer:\n  map: {\n  }\ntail: keep\n").unwrap();
1895        empty_mapping
1896            .add_at(0, &pointer("/outer/map/a"), &fragment("1"))
1897            .unwrap();
1898        assert_eq!(
1899            empty_mapping.as_source(),
1900            "outer:\n  map: {\n    a: 1\n  }\ntail: keep\n"
1901        );
1902        empty_mapping.commit_edits().unwrap();
1903
1904        let mut sequence = YamlDoc::parse("items: [\r\n  a,\r\n  c\r\n]\r\n").unwrap();
1905        sequence
1906            .add_at(0, &pointer("/items/1"), &fragment("b"))
1907            .unwrap();
1908        sequence
1909            .add_at(0, &pointer("/items/-"), &fragment("d"))
1910            .unwrap();
1911        assert_eq!(
1912            sequence.as_source(),
1913            "items: [\r\n  a,\r\n  b,\r\n  c,\r\n  d\r\n]\r\n"
1914        );
1915        sequence.commit_edits().unwrap();
1916
1917        let mut empty_sequence = YamlDoc::parse("items: [\n]\ntail: keep\n").unwrap();
1918        empty_sequence
1919            .add_at(0, &pointer("/items/-"), &fragment("a"))
1920            .unwrap();
1921        assert_eq!(empty_sequence.as_source(), "items: [\n  a\n]\ntail: keep\n");
1922        empty_sequence.commit_edits().unwrap();
1923
1924        let mut compact = YamlDoc::parse("map: {a: 1}\nitems: [a]\n").unwrap();
1925        compact
1926            .add_at(0, &pointer("/map/b"), &fragment("2"))
1927            .unwrap();
1928        compact
1929            .add_at(0, &pointer("/items/-"), &fragment("b"))
1930            .unwrap();
1931        assert_eq!(compact.as_source(), "map: {a: 1, b: 2}\nitems: [a, b]\n");
1932    }
1933
1934    #[test]
1935    fn removes_complete_multiline_block_collection_entries() {
1936        let input = "items:\n  - name: first\n    enabled: true\n  - name: second\n    enabled: false\ntail: keep\n";
1937        let mut sequence = YamlDoc::parse(input).unwrap();
1938        sequence.remove_at(0, &pointer("/items/0")).unwrap();
1939        assert_eq!(
1940            sequence.as_source(),
1941            "items:\n  - name: second\n    enabled: false\ntail: keep\n"
1942        );
1943        sequence.commit_edits().unwrap();
1944
1945        let mut last_sequence = YamlDoc::parse(input).unwrap();
1946        last_sequence.remove_at(0, &pointer("/items/1")).unwrap();
1947        assert_eq!(
1948            last_sequence.as_source(),
1949            "items:\n  - name: first\n    enabled: true\ntail: keep\n"
1950        );
1951        last_sequence.commit_edits().unwrap();
1952
1953        let input = "server:\n  host: localhost\n  tls:\n    enabled: true\ntail: keep\n";
1954        let mut mapping = YamlDoc::parse(input).unwrap();
1955        mapping.remove_at(0, &pointer("/server")).unwrap();
1956        assert_eq!(mapping.as_source(), "tail: keep\n");
1957        mapping.commit_edits().unwrap();
1958    }
1959
1960    #[test]
1961    fn removes_fields_from_compact_sequence_entry_mappings() {
1962        let input = "services:\n  - name: api\n    port: 8080 # public endpoint\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n";
1963
1964        let mut first = YamlDoc::parse(input).unwrap();
1965        first.remove_at(0, &pointer("/services/0/name")).unwrap();
1966        assert_eq!(
1967            first.as_source(),
1968            "services:\n  - port: 8080 # public endpoint\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1969        );
1970        first.commit_edits().unwrap();
1971        assert!(
1972            first
1973                .resolve_pointer(0, &pointer("/services/0/port"))
1974                .is_ok()
1975        );
1976
1977        let mut middle = YamlDoc::parse(input).unwrap();
1978        middle.remove_at(0, &pointer("/services/0/port")).unwrap();
1979        assert_eq!(
1980            middle.as_source(),
1981            "services:\n  - name: api\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1982        );
1983        middle.commit_edits().unwrap();
1984
1985        let mut last = YamlDoc::parse(input).unwrap();
1986        last.remove_at(0, &pointer("/services/0/enabled")).unwrap();
1987        assert_eq!(
1988            last.as_source(),
1989            "services:\n  - name: api\n    port: 8080 # public endpoint\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1990        );
1991        last.commit_edits().unwrap();
1992        assert!(
1993            last.resolve_pointer(0, &pointer("/services/1/name"))
1994                .is_ok()
1995        );
1996
1997        let mut only = YamlDoc::parse("services:\n  - name: api\ntail: keep\n").unwrap();
1998        only.remove_at(0, &pointer("/services/0/name")).unwrap();
1999        assert_eq!(only.as_source(), "services:\n  - {}\ntail: keep\n");
2000        only.commit_edits().unwrap();
2001        let item = only.resolve_pointer(0, &pointer("/services/0")).unwrap();
2002        assert!(matches!(
2003            only.semantic_kind(item),
2004            Some(SemanticKind::Mapping { .. })
2005        ));
2006    }
2007
2008    #[test]
2009    fn removes_terminal_entries_without_collapsing_multiline_flow_layout() {
2010        let mut mapping =
2011            YamlDoc::parse("map: {\n  a: 1, # keep\n  b: 2\n}\ntail: keep\n").unwrap();
2012        mapping.remove_at(0, &pointer("/map/b")).unwrap();
2013        assert_eq!(
2014            mapping.as_source(),
2015            "map: {\n  a: 1 # keep\n}\ntail: keep\n"
2016        );
2017        mapping.commit_edits().unwrap();
2018        assert!(mapping.resolve_pointer(0, &pointer("/map/a")).is_ok());
2019
2020        let mut sequence =
2021            YamlDoc::parse("items: [\r\n  a,\r\n  b\r\n]\r\ntail: keep\r\n").unwrap();
2022        sequence.remove_at(0, &pointer("/items/1")).unwrap();
2023        assert_eq!(
2024            sequence.as_source(),
2025            "items: [\r\n  a\r\n]\r\ntail: keep\r\n"
2026        );
2027        sequence.commit_edits().unwrap();
2028
2029        let mut only = YamlDoc::parse("map: {\n  only: value\n}\ntail: keep\n").unwrap();
2030        only.remove_at(0, &pointer("/map/only")).unwrap();
2031        assert_eq!(only.as_source(), "map: {\n}\ntail: keep\n");
2032        only.commit_edits().unwrap();
2033
2034        let mut compact = YamlDoc::parse("map: {a: 1, b: 2}\n").unwrap();
2035        compact.remove_at(0, &pointer("/map/b")).unwrap();
2036        assert_eq!(compact.as_source(), "map: {a: 1}\n");
2037    }
2038
2039    #[test]
2040    fn removes_multiple_block_sequence_entries_transactionally() {
2041        let input = "items:\n  - name: first\n    enabled: true\n  - name: second\n    enabled: false\ntail: keep\n";
2042        let mut doc = YamlDoc::parse(input).unwrap();
2043        doc.remove_at(0, &pointer("/items/1")).unwrap();
2044        doc.remove_at(0, &pointer("/items/0")).unwrap();
2045        assert_eq!(doc.as_source(), "items: []\ntail: keep\n");
2046        doc.commit_edits().unwrap();
2047    }
2048
2049    #[test]
2050    fn block_removal_owns_only_attached_same_indented_comments() {
2051        let mut mapping = YamlDoc::parse(
2052            "root:\r\n  keep: one\r\n  # first\r\n  # second\r\n  remove: two # inline\r\n  tail: three\r\n",
2053        )
2054        .unwrap();
2055        mapping.remove_at(0, &pointer("/root/remove")).unwrap();
2056        assert_eq!(
2057            mapping.as_source(),
2058            "root:\r\n  keep: one\r\n  tail: three\r\n"
2059        );
2060
2061        let mut separated = YamlDoc::parse(
2062            "root:\n # different indent\n\n  # separated\n  remove: value\n  tail: keep\n",
2063        )
2064        .unwrap();
2065        separated.remove_at(0, &pointer("/root/remove")).unwrap();
2066        assert_eq!(
2067            separated.as_source(),
2068            "root:\n # different indent\n\n  tail: keep\n"
2069        );
2070
2071        let mut sequence = YamlDoc::parse(
2072            "items:\n  - keep\n  # attached\n  - remove\n  # belongs to last\n  - last\n",
2073        )
2074        .unwrap();
2075        sequence.remove_at(0, &pointer("/items/1")).unwrap();
2076        assert_eq!(
2077            sequence.as_source(),
2078            "items:\n  - keep\n  # belongs to last\n  - last\n"
2079        );
2080
2081        let mut edges = YamlDoc::parse(
2082            "root:\n    # different indentation\n  # first comment\n  first:\n    # nested comment\n    value: one\n  middle: keep\n  # last comment\n  last: three\n",
2083        )
2084        .unwrap();
2085        edges.remove_at(0, &pointer("/root/first")).unwrap();
2086        assert_eq!(
2087            edges.as_source(),
2088            "root:\n    # different indentation\n  middle: keep\n  # last comment\n  last: three\n"
2089        );
2090        edges.commit_edits().unwrap();
2091        edges.remove_at(0, &pointer("/root/last")).unwrap();
2092        assert_eq!(
2093            edges.as_source(),
2094            "root:\n    # different indentation\n  middle: keep\n"
2095        );
2096    }
2097
2098    #[test]
2099    fn removals_preserve_empty_block_collection_types() {
2100        let mut nested =
2101            YamlDoc::parse("server:\n  host: localhost\nitems:\n  - only\ntail: keep\n").unwrap();
2102        nested.remove_at(0, &pointer("/server/host")).unwrap();
2103        nested.remove_at(0, &pointer("/items/0")).unwrap();
2104        assert_eq!(nested.as_source(), "server: {}\nitems: []\ntail: keep\n");
2105        nested.commit_edits().unwrap();
2106        assert!(matches!(
2107            nested.semantic_kind(nested.resolve_pointer(0, &pointer("/server")).unwrap()),
2108            Some(SemanticKind::Mapping { .. })
2109        ));
2110        assert!(matches!(
2111            nested.semantic_kind(nested.resolve_pointer(0, &pointer("/items")).unwrap()),
2112            Some(SemanticKind::Sequence { .. })
2113        ));
2114
2115        let mut root_mapping = YamlDoc::parse("only: value\n").unwrap();
2116        root_mapping.remove_at(0, &pointer("/only")).unwrap();
2117        assert_eq!(root_mapping.as_source(), "{}\n");
2118        root_mapping.commit_edits().unwrap();
2119
2120        let mut root_sequence = YamlDoc::parse("- only\n").unwrap();
2121        root_sequence.remove_at(0, &pointer("/0")).unwrap();
2122        assert_eq!(root_sequence.as_source(), "[]\n");
2123        root_sequence.commit_edits().unwrap();
2124
2125        let mut anchored =
2126            YamlDoc::parse("defaults: &defaults\n  retries: 3\nmirror: *defaults\n").unwrap();
2127        anchored
2128            .remove_at(0, &pointer("/defaults/retries"))
2129            .unwrap();
2130        assert_eq!(
2131            anchored.as_source(),
2132            "defaults: &defaults {}\nmirror: *defaults\n"
2133        );
2134        anchored.commit_edits().unwrap();
2135        let mirror = anchored.resolve_pointer(0, &pointer("/mirror")).unwrap();
2136        let resolved = anchored.resolve_alias(mirror).unwrap();
2137        assert!(matches!(
2138            anchored.semantic_kind(resolved),
2139            Some(SemanticKind::Mapping { .. })
2140        ));
2141
2142        let mut commented =
2143            YamlDoc::parse("server: # keep\r\n  host: localhost\r\ntail: keep\r\n").unwrap();
2144        commented.remove_at(0, &pointer("/server/host")).unwrap();
2145        assert_eq!(commented.as_source(), "server: {} # keep\r\ntail: keep\r\n");
2146        commented.commit_edits().unwrap();
2147    }
2148
2149    #[test]
2150    fn moves_can_temporarily_empty_block_collections() {
2151        let mut mapping = YamlDoc::parse("server:\n  host: localhost\ntail: keep\n").unwrap();
2152        mapping
2153            .move_at(0, &pointer("/server/host"), &pointer("/server/name"))
2154            .unwrap();
2155        assert_eq!(
2156            mapping.as_source(),
2157            "server: {name: localhost}\ntail: keep\n"
2158        );
2159
2160        let mut sequence = YamlDoc::parse("items:\n  - only\ntail: keep\n").unwrap();
2161        sequence
2162            .move_at(0, &pointer("/items/0"), &pointer("/items/-"))
2163            .unwrap();
2164        assert_eq!(sequence.as_source(), "items: [only]\ntail: keep\n");
2165    }
2166
2167    #[test]
2168    fn mutations_are_transactional() {
2169        let input = "items: [a]\n";
2170        let mut doc = YamlDoc::parse(input).unwrap();
2171        assert!(doc.add_at(0, &pointer("/items/4"), &fragment("x")).is_err());
2172        assert_eq!(doc.as_source(), input);
2173    }
2174
2175    #[test]
2176    fn move_uses_remove_then_add_sequence_indices() {
2177        let mut doc = YamlDoc::parse("[a, b, c]\n").unwrap();
2178        doc.move_at(0, &pointer("/0"), &pointer("/2")).unwrap();
2179        assert_eq!(doc.as_source(), "[b, c, a]\n");
2180    }
2181
2182    #[test]
2183    fn move_and_copy_strip_inline_comments_when_rendering_flow_values() {
2184        let input = "value: 8080 # public endpoint\ntarget: {}\n";
2185        let mut moved = YamlDoc::parse(input).unwrap();
2186        moved
2187            .move_at(0, &pointer("/value"), &pointer("/target/moved"))
2188            .unwrap();
2189        assert_eq!(moved.as_source(), "target: {moved: 8080}\n");
2190        moved.commit_edits().unwrap();
2191
2192        let input = "value: \"a # b\" # keep here\ntarget: {}\n";
2193        let mut copied = YamlDoc::parse(input).unwrap();
2194        copied
2195            .copy_at(0, &pointer("/value"), &pointer("/target/copied"))
2196            .unwrap();
2197        assert_eq!(
2198            copied.as_source(),
2199            "value: \"a # b\" # keep here\ntarget: {copied: \"a # b\"}\n"
2200        );
2201        copied.commit_edits().unwrap();
2202    }
2203
2204    #[test]
2205    fn copy_rejects_anchors_and_test_is_semantic() {
2206        let mut doc = YamlDoc::parse("one: &one {value: 1}\ntwo: null\n").unwrap();
2207        assert!(
2208            doc.copy_at(0, &pointer("/one"), &pointer("/two"))
2209                .unwrap_err()
2210                .to_string()
2211                .contains("anchor")
2212        );
2213        assert!(
2214            doc.test_at(0, &pointer("/one/value"), &fragment("1.0"))
2215                .unwrap()
2216        );
2217    }
2218}