Skip to main content

yaml_rt_core/
edit.rs

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