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 mut insertion = insertion_prefix(self, offset);
837                let value = value.prepared(self)?.to_yaml()?;
838                insertion.push_str(&format_block_mapping_entry(
839                    &key,
840                    &value,
841                    indent,
842                    self.preferred_line_ending(),
843                ));
844                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
845            }
846        }
847        Ok(())
848    }
849
850    pub(crate) fn queue_mapping_insert_before(
851        &mut self,
852        mapping: NodeId,
853        before_entry: NodeId,
854        key: &str,
855        value: &YamlFragment,
856    ) -> Result<(), YamlEditError> {
857        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
858            return Err(YamlEditError::new(
859                "mapping insertion target is not a mapping",
860            ));
861        };
862        if style != CollectionStyle::Flow {
863            return Err(YamlEditError::new(
864                "ordered flow insertion requires a flow mapping",
865            ));
866        }
867        let entry = self.expect_node(before_entry)?;
868        let key = emit_string_key(key);
869        let value = value.render_flow(self)?;
870        if self.flow_collection_is_multiline(mapping)? {
871            let content_start = self.flow_entry_content_start(before_entry)?;
872            let line_start = self.line_start_offset(content_start);
873            if line_start != self.line_start_offset(self.expect_node(mapping)?.span.start as usize)
874            {
875                let indent = &self.source().as_str()[line_start..content_start];
876                self.queue_edit(
877                    Span::empty_from_usize(line_start),
878                    format!("{indent}{key}: {value},{}", self.preferred_line_ending()),
879                )?;
880                return Ok(());
881            }
882        }
883        self.queue_edit(Span::empty(entry.span.start), format!("{key}: {value}, "))?;
884        Ok(())
885    }
886
887    pub(crate) fn queue_sequence_insert(
888        &mut self,
889        sequence: NodeId,
890        index: usize,
891        value: &YamlFragment,
892    ) -> Result<(), YamlEditError> {
893        let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
894            return Err(YamlEditError::new(
895                "sequence insertion target is not a sequence",
896            ));
897        };
898        let items = self.sequence_items(sequence).collect::<Vec<_>>();
899        match style {
900            CollectionStyle::Flow => {
901                let sequence_span = self.expect_node(sequence)?.span;
902                let entries = self
903                    .children(sequence)
904                    .filter(|node| self.containing_entry_child(*node))
905                    .collect::<Vec<_>>();
906                let close = closing_delimiter_offset(self, sequence_span, ']')?;
907                let value = value.render_flow(self)?;
908                if index == items.len()
909                    && self.queue_multiline_flow_append(sequence, close, &entries, &value)?
910                {
911                    return Ok(());
912                }
913                if let Some(entry) = entries.get(index).copied()
914                    && self.flow_collection_is_multiline(sequence)?
915                {
916                    let content_start = self.flow_entry_content_start(entry)?;
917                    let line_start = self.line_start_offset(content_start);
918                    if line_start != self.line_start_offset(sequence_span.start as usize) {
919                        let indent = &self.source().as_str()[line_start..content_start];
920                        self.queue_edit(
921                            Span::empty_from_usize(line_start),
922                            format!("{indent}{value},{}", self.preferred_line_ending()),
923                        )?;
924                        return Ok(());
925                    }
926                }
927                let offset = if let Some(item) = items.get(index).copied() {
928                    self.expect_node(item)?.span.start as usize
929                } else {
930                    close
931                };
932                let insertion = if items.is_empty() {
933                    value
934                } else if index < items.len() {
935                    format!("{value}, ")
936                } else {
937                    format!(", {value}")
938                };
939                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
940            }
941            CollectionStyle::Block => {
942                let sequence_node = self.expect_node(sequence)?;
943                let indent = self.node_indent(sequence_node);
944                let offset = if let Some(item) = items.get(index).copied() {
945                    let entry = self.containing_entry(item).unwrap_or(item);
946                    self.line_start_offset(self.expect_node(entry)?.span.start as usize)
947                } else {
948                    self.sequence_insertion_offset(sequence_node)
949                };
950                let mut insertion = insertion_prefix(self, offset);
951                let value = value.prepared(self)?.to_yaml()?;
952                insertion.push_str(&format_block_sequence_entry(
953                    &value,
954                    indent,
955                    self.preferred_line_ending(),
956                ));
957                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
958            }
959        }
960        Ok(())
961    }
962
963    fn flow_entry_content_start(&self, entry: NodeId) -> Result<usize, YamlEditError> {
964        self.semantic_children(entry)
965            .next()
966            .and_then(|value| self.node(value))
967            .map(|value| value.span.start as usize)
968            .ok_or_else(|| YamlEditError::new("flow collection entry does not contain a value"))
969    }
970
971    fn queue_multiline_flow_append(
972        &mut self,
973        collection: NodeId,
974        close: usize,
975        entries: &[NodeId],
976        rendered: &str,
977    ) -> Result<bool, YamlEditError> {
978        if !self.flow_collection_is_multiline(collection)? {
979            return Ok(false);
980        }
981        let close_line = self.line_start_offset(close);
982        let indent = if let Some(last) = entries.last().copied() {
983            let content_start = self.flow_entry_content_start(last)?;
984            if self.line_start_offset(content_start) == close_line {
985                return Ok(false);
986            }
987            self.source().as_str()[self.line_start_offset(content_start)..content_start].to_owned()
988        } else {
989            let close_indent = self.source().as_str()[close_line..close]
990                .bytes()
991                .take_while(|byte| *byte == b' ')
992                .count();
993            " ".repeat(close_indent + 2)
994        };
995        if let Some(last) = entries.last().copied() {
996            let separator = Span::empty_from_usize(self.flow_entry_value_end(last)?);
997            self.queue_edit(separator, ",".to_owned())?;
998        }
999        self.queue_edit(
1000            Span::empty_from_usize(close_line),
1001            format!("{indent}{rendered}{}", self.preferred_line_ending()),
1002        )?;
1003        Ok(true)
1004    }
1005
1006    pub(crate) fn is_flow_context(&self, mut node: NodeId) -> bool {
1007        while let Some(parent) = self.node(node).and_then(super::syntax::Node::parent) {
1008            if matches!(
1009                self.semantic_kind(parent),
1010                Some(
1011                    SemanticKind::Mapping {
1012                        style: CollectionStyle::Flow
1013                    } | SemanticKind::Sequence {
1014                        style: CollectionStyle::Flow
1015                    }
1016                )
1017            ) {
1018                return true;
1019            }
1020            node = parent;
1021        }
1022        false
1023    }
1024
1025    fn anchor_has_external_alias(&self, root: NodeId) -> bool {
1026        let Some(root_span) = self.node(root).map(super::syntax::Node::span) else {
1027            return false;
1028        };
1029        let anchored = self
1030            .nodes
1031            .iter()
1032            .enumerate()
1033            .map(|(index, _)| NodeId::from_usize(index))
1034            .filter(|node| {
1035                self.anchor(*node).is_some()
1036                    && self.node(*node).is_some_and(|node| {
1037                        node.span().start >= root_span.start && node.span().end <= root_span.end
1038                    })
1039            })
1040            .collect::<Vec<_>>();
1041        self.nodes
1042            .iter()
1043            .enumerate()
1044            .map(|(index, _)| NodeId::from_usize(index))
1045            .filter(|node| matches!(self.semantic_kind(*node), Some(SemanticKind::Alias)))
1046            .any(|alias| {
1047                let outside = self.node(alias).is_some_and(|node| {
1048                    node.span().start < root_span.start || node.span().end > root_span.end
1049                });
1050                outside
1051                    && self
1052                        .resolve_alias(alias)
1053                        .is_some_and(|target| anchored.contains(&target))
1054            })
1055    }
1056
1057    pub(crate) fn line_start_offset(&self, offset: usize) -> usize {
1058        let offset = u32::try_from(offset).unwrap_or(u32::MAX);
1059        match self.source.line_starts().binary_search(&offset) {
1060            Ok(index) => self.source.line_starts()[index] as usize,
1061            Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
1062        }
1063    }
1064}
1065
1066pub(crate) fn closing_delimiter_offset(
1067    doc: &YamlDoc,
1068    span: Span,
1069    delimiter: char,
1070) -> Result<usize, YamlEditError> {
1071    let source = doc.source.slice(span);
1072    let relative = source
1073        .rfind(delimiter)
1074        .ok_or_else(|| YamlEditError::new(format!("missing `{delimiter}` delimiter")))?;
1075    Ok(span.start as usize + relative)
1076}
1077
1078fn insertion_prefix(doc: &YamlDoc, offset: usize) -> String {
1079    if offset == doc.source.len()
1080        && !doc
1081            .source
1082            .as_str()
1083            .as_bytes()
1084            .last()
1085            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
1086    {
1087        doc.preferred_line_ending().to_owned()
1088    } else {
1089        String::new()
1090    }
1091}
1092
1093fn format_block_mapping_entry(key: &str, value: &str, indent: usize, ending: &str) -> String {
1094    let prefix = " ".repeat(indent);
1095    if !value.contains(['\n', '\r']) {
1096        return format!("{prefix}{key}: {value}{ending}");
1097    }
1098    let value = indent_text(value, indent + 2);
1099    let mut output = format!("{prefix}{key}:{ending}{value}");
1100    if !output.ends_with(['\n', '\r']) {
1101        output.push_str(ending);
1102    }
1103    output
1104}
1105
1106fn format_block_sequence_entry(value: &str, indent: usize, ending: &str) -> String {
1107    let prefix = " ".repeat(indent);
1108    if !value.contains(['\n', '\r']) {
1109        return format!("{prefix}- {value}{ending}");
1110    }
1111    let value = indent_text(value, indent + 2);
1112    let mut output = format!("{prefix}-{ending}{value}");
1113    if !output.ends_with(['\n', '\r']) {
1114        output.push_str(ending);
1115    }
1116    output
1117}
1118
1119fn indent_continuation_lines(value: &str, indent: usize) -> String {
1120    if indent == 0 {
1121        return value.to_owned();
1122    }
1123    let prefix = " ".repeat(indent);
1124    let mut output = String::with_capacity(value.len());
1125    let mut after_break = false;
1126    for character in value.chars() {
1127        if after_break && !matches!(character, '\r' | '\n') {
1128            output.push_str(&prefix);
1129            after_break = false;
1130        }
1131        output.push(character);
1132        if character == '\n' {
1133            after_break = true;
1134        } else if character != '\r' {
1135            after_break = false;
1136        }
1137    }
1138    output
1139}
1140
1141pub(crate) fn emit_string_key(value: &str) -> String {
1142    if safe_plain_string(value) {
1143        value.to_owned()
1144    } else {
1145        crate::fragment::quote_string(value)
1146    }
1147}
1148
1149pub(crate) fn safe_plain_string(value: &str) -> bool {
1150    if !safe_plain_string_syntax(value) {
1151        return false;
1152    }
1153    matches!(
1154        resolve_scalar(value, YamlScalarStyle::Plain, None),
1155        Ok(ResolvedScalar::String)
1156    )
1157}
1158
1159fn safe_plain_string_syntax(value: &str) -> bool {
1160    if value.is_empty()
1161        || value.trim() != value
1162        || value.contains(['\n', '\r', '\t', ':', '#', '[', ']', '{', '}', ','])
1163        || value.starts_with(['-', '?', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'])
1164    {
1165        return false;
1166    }
1167    true
1168}
1169
1170fn safe_plain_key_with_tag(value: &str, tag: Option<&str>) -> bool {
1171    const STRING_TAG: &str = "tag:yaml.org,2002:str";
1172
1173    safe_plain_string_syntax(value)
1174        && (tag == Some(STRING_TAG)
1175            || matches!(
1176                resolve_scalar(value, YamlScalarStyle::Plain, None),
1177                Ok(ResolvedScalar::String)
1178            ))
1179}
1180
1181fn scalar_is_string(doc: &YamlDoc, node: NodeId) -> Result<bool, YamlEditError> {
1182    let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(node) else {
1183        return Ok(false);
1184    };
1185    let value = doc.scalar_value(node)?;
1186    let tag = doc.resolved_tag(node)?;
1187    Ok(matches!(
1188        resolve_scalar(&value, style, tag.as_deref()),
1189        Ok(ResolvedScalar::String)
1190    ))
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196
1197    fn pointer(value: &str) -> JsonPointer {
1198        JsonPointer::parse(value).unwrap()
1199    }
1200
1201    fn fragment(value: &str) -> YamlFragment {
1202        YamlFragment::parse(value).unwrap()
1203    }
1204
1205    #[test]
1206    fn renames_block_flow_and_explicit_mapping_keys_losslessly() {
1207        let input = "old: 1 # keep\nflow: {old: 2}\nexplicit:\n  ? 'old'\n  : 3\n";
1208        let mut doc = YamlDoc::parse(input).unwrap();
1209        doc.rename_keys_at(
1210            0,
1211            &[
1212                pointer("/old"),
1213                pointer("/flow/old"),
1214                pointer("/explicit/old"),
1215            ],
1216            "true",
1217        )
1218        .unwrap();
1219
1220        assert_eq!(
1221            doc.as_source(),
1222            "\"true\": 1 # keep\nflow: {\"true\": 2}\nexplicit:\n  ? 'true'\n  : 3\n"
1223        );
1224    }
1225
1226    #[test]
1227    fn rename_key_quotes_plain_names_that_are_not_safe_strings() {
1228        for new_key in ["true", "", "a: b", "line\nbreak"] {
1229            let mut doc = YamlDoc::parse("old: value\n").unwrap();
1230            doc.rename_key_at(0, &pointer("/old"), new_key).unwrap();
1231            assert_eq!(
1232                doc.as_source(),
1233                format!("{}: value\n", crate::fragment::quote_string(new_key))
1234            );
1235        }
1236
1237        let mut flow = YamlDoc::parse("{old: value}\n").unwrap();
1238        flow.rename_key_at(0, &pointer("/old"), "a,b").unwrap();
1239        assert_eq!(flow.as_source(), "{\"a,b\": value}\n");
1240    }
1241
1242    #[test]
1243    fn rename_key_preserves_quoted_styles_properties_comments_and_line_endings() {
1244        let mut single = YamlDoc::parse("'old': value\n").unwrap();
1245        single.rename_key_at(0, &pointer("/old"), "Bob's").unwrap();
1246        assert_eq!(single.as_source(), "'Bob''s': value\n");
1247
1248        let mut double = YamlDoc::parse("\"old\": value\n").unwrap();
1249        double
1250            .rename_key_at(0, &pointer("/old"), "new \"key\"")
1251            .unwrap();
1252        assert_eq!(double.as_source(), "\"new \\\"key\\\"\": value\n");
1253
1254        let mut tagged = YamlDoc::parse("!!str &key old: value # keep\r\n").unwrap();
1255        tagged.rename_key_at(0, &pointer("/old"), "true").unwrap();
1256        assert_eq!(tagged.as_source(), "!!str &key true: value # keep\r\n");
1257    }
1258
1259    #[test]
1260    fn rename_keys_resolves_all_targets_before_editing_and_deduplicates_alias_routes() {
1261        let mut nested = YamlDoc::parse("parent:\n  old: 1\nold: 2\n").unwrap();
1262        nested
1263            .rename_keys_at(0, &[pointer("/parent/old"), pointer("/parent")], "renamed")
1264            .unwrap();
1265        assert_eq!(nested.as_source(), "renamed:\n  renamed: 1\nold: 2\n");
1266
1267        let mut aliases = YamlDoc::parse("base: &base\n  name: Ada\ncopy: *base\n").unwrap();
1268        aliases
1269            .rename_keys_at(
1270                0,
1271                &[pointer("/base/name"), pointer("/copy/name")],
1272                "display-name",
1273            )
1274            .unwrap();
1275        assert_eq!(
1276            aliases.as_source(),
1277            "base: &base\n  display-name: Ada\ncopy: *base\n"
1278        );
1279    }
1280
1281    #[test]
1282    fn rename_keys_rejects_collisions_transactionally() {
1283        let input = "a: 1\nb: 2\n";
1284        let mut existing = YamlDoc::parse(input).unwrap();
1285        let error = existing.rename_key_at(0, &pointer("/a"), "b").unwrap_err();
1286        assert!(error.to_string().contains("duplicate key \"b\""));
1287        assert_eq!(existing.as_source(), input);
1288
1289        let mut selected = YamlDoc::parse(input).unwrap();
1290        let error = selected
1291            .rename_keys_at(0, &[pointer("/a"), pointer("/b")], "x")
1292            .unwrap_err();
1293        assert!(error.to_string().contains("duplicate key \"x\""));
1294        assert_eq!(selected.as_source(), input);
1295    }
1296
1297    #[test]
1298    fn rename_keys_treats_duplicate_and_unchanged_targets_as_no_ops() {
1299        let input = "\"old\": value\n";
1300        let mut doc = YamlDoc::parse(input).unwrap();
1301        doc.rename_keys_at(0, &[pointer("/old"), pointer("/old")], "old")
1302            .unwrap();
1303        doc.rename_keys_at(0, &[], "ignored").unwrap();
1304        assert_eq!(doc.as_source(), input);
1305    }
1306
1307    #[test]
1308    fn rename_key_rejects_non_members_and_unsupported_key_forms() {
1309        let mut root = YamlDoc::parse("key: value\n").unwrap();
1310        assert!(
1311            root.rename_key_at(0, &pointer(""), "new")
1312                .unwrap_err()
1313                .to_string()
1314                .contains("document root")
1315        );
1316        assert_eq!(root.as_source(), "key: value\n");
1317
1318        let mut sequence = YamlDoc::parse("- value\n").unwrap();
1319        assert!(
1320            sequence
1321                .rename_key_at(0, &pointer("/0"), "new")
1322                .unwrap_err()
1323                .to_string()
1324                .contains("does not select a mapping member")
1325        );
1326
1327        let mut block = YamlDoc::parse("? >\n  old\n: value\n").unwrap();
1328        assert!(
1329            block
1330                .rename_key_at(0, &pointer("/old\n"), "new")
1331                .unwrap_err()
1332                .to_string()
1333                .contains("plain, single-quoted, and double-quoted")
1334        );
1335
1336        let mut alias = YamlDoc::parse("name: &key target\n? *key\n: value\n").unwrap();
1337        let error = alias
1338            .rename_key_at(0, &pointer("/target"), "new")
1339            .unwrap_err();
1340        assert!(
1341            error
1342                .to_string()
1343                .contains("plain, single-quoted, and double-quoted"),
1344            "{error}"
1345        );
1346
1347        let mut complex = YamlDoc::parse("? [a, b]\n: value\n").unwrap();
1348        assert!(
1349            complex
1350                .rename_key_at(0, &pointer("/anything"), "new")
1351                .is_err()
1352        );
1353    }
1354
1355    #[test]
1356    fn adds_replaces_and_removes_block_mapping_values() {
1357        let mut doc = YamlDoc::parse("server:\n  host: localhost # keep\n").unwrap();
1358        doc.add_at(0, &pointer("/server/port"), &fragment("8080"))
1359            .unwrap();
1360        assert_eq!(
1361            doc.as_source(),
1362            "server:\n  host: localhost # keep\n  port: 8080\n"
1363        );
1364        doc.replace_at(0, &pointer("/server/host"), &fragment("example.com"))
1365            .unwrap();
1366        assert_eq!(
1367            doc.as_source(),
1368            "server:\n  host: example.com # keep\n  port: 8080\n"
1369        );
1370        doc.remove_at(0, &pointer("/server/port")).unwrap();
1371        assert_eq!(doc.as_source(), "server:\n  host: example.com # keep\n");
1372    }
1373
1374    #[test]
1375    fn inserts_into_compact_sequence_entry_mappings_at_the_key_column() {
1376        let input = "services:\n  - name: api\n    port: 8080\n";
1377
1378        let mut scalar = YamlDoc::parse(input).unwrap();
1379        scalar
1380            .add_at(0, &pointer("/services/0/enabled"), &fragment("true"))
1381            .unwrap();
1382        assert_eq!(
1383            scalar.as_source(),
1384            "services:\n  - name: api\n    port: 8080\n    enabled: true\n"
1385        );
1386        scalar.commit_edits().unwrap();
1387
1388        let mut nested = YamlDoc::parse(input).unwrap();
1389        nested
1390            .add_at(
1391                0,
1392                &pointer("/services/0/tls"),
1393                &fragment("{enabled: true, mode: strict}"),
1394            )
1395            .unwrap();
1396        assert_eq!(
1397            nested.as_source(),
1398            "services:\n  - name: api\n    port: 8080\n    tls: {enabled: true, mode: strict}\n"
1399        );
1400        nested.commit_edits().unwrap();
1401    }
1402
1403    #[test]
1404    fn adds_string_keys_without_changing_their_schema_type() {
1405        let mut doc = YamlDoc::parse("{}\n").unwrap();
1406        doc.add_at(0, &pointer("/true"), &fragment("value"))
1407            .unwrap();
1408        assert_eq!(doc.as_source(), "{\"true\": value}\n");
1409        assert!(
1410            doc.resolve_pointer(0, &pointer("/true")).is_ok(),
1411            "{}",
1412            doc.as_source()
1413        );
1414    }
1415
1416    #[test]
1417    fn inserts_block_and_flow_sequence_items() {
1418        let mut block = YamlDoc::parse("items:\n  - a\n  - c\n").unwrap();
1419        block
1420            .add_at(0, &pointer("/items/1"), &fragment("b"))
1421            .unwrap();
1422        block
1423            .add_at(0, &pointer("/items/-"), &fragment("d"))
1424            .unwrap();
1425        assert_eq!(block.as_source(), "items:\n  - a\n  - b\n  - c\n  - d\n");
1426
1427        let mut flow = YamlDoc::parse("items: [a, c]\n").unwrap();
1428        flow.add_at(0, &pointer("/items/1"), &fragment("b"))
1429            .unwrap();
1430        assert_eq!(flow.as_source(), "items: [a, b, c]\n");
1431    }
1432
1433    #[test]
1434    fn inserts_into_multiline_flow_collections_without_collapsing_layout() {
1435        let mut mapping = YamlDoc::parse("map: {\n  a: 1 # keep\n}\ntail: keep\n").unwrap();
1436        mapping
1437            .add_at(0, &pointer("/map/b"), &fragment("2"))
1438            .unwrap();
1439        assert_eq!(
1440            mapping.as_source(),
1441            "map: {\n  a: 1, # keep\n  b: 2\n}\ntail: keep\n"
1442        );
1443        mapping.commit_edits().unwrap();
1444
1445        let mut empty_mapping = YamlDoc::parse("outer:\n  map: {\n  }\ntail: keep\n").unwrap();
1446        empty_mapping
1447            .add_at(0, &pointer("/outer/map/a"), &fragment("1"))
1448            .unwrap();
1449        assert_eq!(
1450            empty_mapping.as_source(),
1451            "outer:\n  map: {\n    a: 1\n  }\ntail: keep\n"
1452        );
1453        empty_mapping.commit_edits().unwrap();
1454
1455        let mut sequence = YamlDoc::parse("items: [\r\n  a,\r\n  c\r\n]\r\n").unwrap();
1456        sequence
1457            .add_at(0, &pointer("/items/1"), &fragment("b"))
1458            .unwrap();
1459        sequence
1460            .add_at(0, &pointer("/items/-"), &fragment("d"))
1461            .unwrap();
1462        assert_eq!(
1463            sequence.as_source(),
1464            "items: [\r\n  a,\r\n  b,\r\n  c,\r\n  d\r\n]\r\n"
1465        );
1466        sequence.commit_edits().unwrap();
1467
1468        let mut empty_sequence = YamlDoc::parse("items: [\n]\ntail: keep\n").unwrap();
1469        empty_sequence
1470            .add_at(0, &pointer("/items/-"), &fragment("a"))
1471            .unwrap();
1472        assert_eq!(empty_sequence.as_source(), "items: [\n  a\n]\ntail: keep\n");
1473        empty_sequence.commit_edits().unwrap();
1474
1475        let mut compact = YamlDoc::parse("map: {a: 1}\nitems: [a]\n").unwrap();
1476        compact
1477            .add_at(0, &pointer("/map/b"), &fragment("2"))
1478            .unwrap();
1479        compact
1480            .add_at(0, &pointer("/items/-"), &fragment("b"))
1481            .unwrap();
1482        assert_eq!(compact.as_source(), "map: {a: 1, b: 2}\nitems: [a, b]\n");
1483    }
1484
1485    #[test]
1486    fn removes_complete_multiline_block_collection_entries() {
1487        let input = "items:\n  - name: first\n    enabled: true\n  - name: second\n    enabled: false\ntail: keep\n";
1488        let mut sequence = YamlDoc::parse(input).unwrap();
1489        sequence.remove_at(0, &pointer("/items/0")).unwrap();
1490        assert_eq!(
1491            sequence.as_source(),
1492            "items:\n  - name: second\n    enabled: false\ntail: keep\n"
1493        );
1494        sequence.commit_edits().unwrap();
1495
1496        let mut last_sequence = YamlDoc::parse(input).unwrap();
1497        last_sequence.remove_at(0, &pointer("/items/1")).unwrap();
1498        assert_eq!(
1499            last_sequence.as_source(),
1500            "items:\n  - name: first\n    enabled: true\ntail: keep\n"
1501        );
1502        last_sequence.commit_edits().unwrap();
1503
1504        let input = "server:\n  host: localhost\n  tls:\n    enabled: true\ntail: keep\n";
1505        let mut mapping = YamlDoc::parse(input).unwrap();
1506        mapping.remove_at(0, &pointer("/server")).unwrap();
1507        assert_eq!(mapping.as_source(), "tail: keep\n");
1508        mapping.commit_edits().unwrap();
1509    }
1510
1511    #[test]
1512    fn removes_fields_from_compact_sequence_entry_mappings() {
1513        let input = "services:\n  - name: api\n    port: 8080 # public endpoint\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n";
1514
1515        let mut first = YamlDoc::parse(input).unwrap();
1516        first.remove_at(0, &pointer("/services/0/name")).unwrap();
1517        assert_eq!(
1518            first.as_source(),
1519            "services:\n  - port: 8080 # public endpoint\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1520        );
1521        first.commit_edits().unwrap();
1522        assert!(
1523            first
1524                .resolve_pointer(0, &pointer("/services/0/port"))
1525                .is_ok()
1526        );
1527
1528        let mut middle = YamlDoc::parse(input).unwrap();
1529        middle.remove_at(0, &pointer("/services/0/port")).unwrap();
1530        assert_eq!(
1531            middle.as_source(),
1532            "services:\n  - name: api\n    enabled: TRUE\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1533        );
1534        middle.commit_edits().unwrap();
1535
1536        let mut last = YamlDoc::parse(input).unwrap();
1537        last.remove_at(0, &pointer("/services/0/enabled")).unwrap();
1538        assert_eq!(
1539            last.as_source(),
1540            "services:\n  - name: api\n    port: 8080 # public endpoint\n  - {name: worker, port: 8081, enabled: false}\ntail: keep\n"
1541        );
1542        last.commit_edits().unwrap();
1543        assert!(
1544            last.resolve_pointer(0, &pointer("/services/1/name"))
1545                .is_ok()
1546        );
1547
1548        let mut only = YamlDoc::parse("services:\n  - name: api\ntail: keep\n").unwrap();
1549        only.remove_at(0, &pointer("/services/0/name")).unwrap();
1550        assert_eq!(only.as_source(), "services:\n  - {}\ntail: keep\n");
1551        only.commit_edits().unwrap();
1552        let item = only.resolve_pointer(0, &pointer("/services/0")).unwrap();
1553        assert!(matches!(
1554            only.semantic_kind(item),
1555            Some(SemanticKind::Mapping { .. })
1556        ));
1557    }
1558
1559    #[test]
1560    fn removes_terminal_entries_without_collapsing_multiline_flow_layout() {
1561        let mut mapping =
1562            YamlDoc::parse("map: {\n  a: 1, # keep\n  b: 2\n}\ntail: keep\n").unwrap();
1563        mapping.remove_at(0, &pointer("/map/b")).unwrap();
1564        assert_eq!(
1565            mapping.as_source(),
1566            "map: {\n  a: 1 # keep\n}\ntail: keep\n"
1567        );
1568        mapping.commit_edits().unwrap();
1569        assert!(mapping.resolve_pointer(0, &pointer("/map/a")).is_ok());
1570
1571        let mut sequence =
1572            YamlDoc::parse("items: [\r\n  a,\r\n  b\r\n]\r\ntail: keep\r\n").unwrap();
1573        sequence.remove_at(0, &pointer("/items/1")).unwrap();
1574        assert_eq!(
1575            sequence.as_source(),
1576            "items: [\r\n  a\r\n]\r\ntail: keep\r\n"
1577        );
1578        sequence.commit_edits().unwrap();
1579
1580        let mut only = YamlDoc::parse("map: {\n  only: value\n}\ntail: keep\n").unwrap();
1581        only.remove_at(0, &pointer("/map/only")).unwrap();
1582        assert_eq!(only.as_source(), "map: {\n}\ntail: keep\n");
1583        only.commit_edits().unwrap();
1584
1585        let mut compact = YamlDoc::parse("map: {a: 1, b: 2}\n").unwrap();
1586        compact.remove_at(0, &pointer("/map/b")).unwrap();
1587        assert_eq!(compact.as_source(), "map: {a: 1}\n");
1588    }
1589
1590    #[test]
1591    fn removes_multiple_block_sequence_entries_transactionally() {
1592        let input = "items:\n  - name: first\n    enabled: true\n  - name: second\n    enabled: false\ntail: keep\n";
1593        let mut doc = YamlDoc::parse(input).unwrap();
1594        doc.remove_at(0, &pointer("/items/1")).unwrap();
1595        doc.remove_at(0, &pointer("/items/0")).unwrap();
1596        assert_eq!(doc.as_source(), "items: []\ntail: keep\n");
1597        doc.commit_edits().unwrap();
1598    }
1599
1600    #[test]
1601    fn removals_preserve_empty_block_collection_types() {
1602        let mut nested =
1603            YamlDoc::parse("server:\n  host: localhost\nitems:\n  - only\ntail: keep\n").unwrap();
1604        nested.remove_at(0, &pointer("/server/host")).unwrap();
1605        nested.remove_at(0, &pointer("/items/0")).unwrap();
1606        assert_eq!(nested.as_source(), "server: {}\nitems: []\ntail: keep\n");
1607        nested.commit_edits().unwrap();
1608        assert!(matches!(
1609            nested.semantic_kind(nested.resolve_pointer(0, &pointer("/server")).unwrap()),
1610            Some(SemanticKind::Mapping { .. })
1611        ));
1612        assert!(matches!(
1613            nested.semantic_kind(nested.resolve_pointer(0, &pointer("/items")).unwrap()),
1614            Some(SemanticKind::Sequence { .. })
1615        ));
1616
1617        let mut root_mapping = YamlDoc::parse("only: value\n").unwrap();
1618        root_mapping.remove_at(0, &pointer("/only")).unwrap();
1619        assert_eq!(root_mapping.as_source(), "{}\n");
1620        root_mapping.commit_edits().unwrap();
1621
1622        let mut root_sequence = YamlDoc::parse("- only\n").unwrap();
1623        root_sequence.remove_at(0, &pointer("/0")).unwrap();
1624        assert_eq!(root_sequence.as_source(), "[]\n");
1625        root_sequence.commit_edits().unwrap();
1626
1627        let mut anchored =
1628            YamlDoc::parse("defaults: &defaults\n  retries: 3\nmirror: *defaults\n").unwrap();
1629        anchored
1630            .remove_at(0, &pointer("/defaults/retries"))
1631            .unwrap();
1632        assert_eq!(
1633            anchored.as_source(),
1634            "defaults: &defaults {}\nmirror: *defaults\n"
1635        );
1636        anchored.commit_edits().unwrap();
1637        let mirror = anchored.resolve_pointer(0, &pointer("/mirror")).unwrap();
1638        let resolved = anchored.resolve_alias(mirror).unwrap();
1639        assert!(matches!(
1640            anchored.semantic_kind(resolved),
1641            Some(SemanticKind::Mapping { .. })
1642        ));
1643
1644        let mut commented =
1645            YamlDoc::parse("server: # keep\r\n  host: localhost\r\ntail: keep\r\n").unwrap();
1646        commented.remove_at(0, &pointer("/server/host")).unwrap();
1647        assert_eq!(commented.as_source(), "server: {} # keep\r\ntail: keep\r\n");
1648        commented.commit_edits().unwrap();
1649    }
1650
1651    #[test]
1652    fn moves_can_temporarily_empty_block_collections() {
1653        let mut mapping = YamlDoc::parse("server:\n  host: localhost\ntail: keep\n").unwrap();
1654        mapping
1655            .move_at(0, &pointer("/server/host"), &pointer("/server/name"))
1656            .unwrap();
1657        assert_eq!(
1658            mapping.as_source(),
1659            "server: {name: localhost}\ntail: keep\n"
1660        );
1661
1662        let mut sequence = YamlDoc::parse("items:\n  - only\ntail: keep\n").unwrap();
1663        sequence
1664            .move_at(0, &pointer("/items/0"), &pointer("/items/-"))
1665            .unwrap();
1666        assert_eq!(sequence.as_source(), "items: [only]\ntail: keep\n");
1667    }
1668
1669    #[test]
1670    fn mutations_are_transactional() {
1671        let input = "items: [a]\n";
1672        let mut doc = YamlDoc::parse(input).unwrap();
1673        assert!(doc.add_at(0, &pointer("/items/4"), &fragment("x")).is_err());
1674        assert_eq!(doc.as_source(), input);
1675    }
1676
1677    #[test]
1678    fn move_uses_remove_then_add_sequence_indices() {
1679        let mut doc = YamlDoc::parse("[a, b, c]\n").unwrap();
1680        doc.move_at(0, &pointer("/0"), &pointer("/2")).unwrap();
1681        assert_eq!(doc.as_source(), "[b, c, a]\n");
1682    }
1683
1684    #[test]
1685    fn move_and_copy_strip_inline_comments_when_rendering_flow_values() {
1686        let input = "value: 8080 # public endpoint\ntarget: {}\n";
1687        let mut moved = YamlDoc::parse(input).unwrap();
1688        moved
1689            .move_at(0, &pointer("/value"), &pointer("/target/moved"))
1690            .unwrap();
1691        assert_eq!(moved.as_source(), "target: {moved: 8080}\n");
1692        moved.commit_edits().unwrap();
1693
1694        let input = "value: \"a # b\" # keep here\ntarget: {}\n";
1695        let mut copied = YamlDoc::parse(input).unwrap();
1696        copied
1697            .copy_at(0, &pointer("/value"), &pointer("/target/copied"))
1698            .unwrap();
1699        assert_eq!(
1700            copied.as_source(),
1701            "value: \"a # b\" # keep here\ntarget: {copied: \"a # b\"}\n"
1702        );
1703        copied.commit_edits().unwrap();
1704    }
1705
1706    #[test]
1707    fn copy_rejects_anchors_and_test_is_semantic() {
1708        let mut doc = YamlDoc::parse("one: &one {value: 1}\ntwo: null\n").unwrap();
1709        assert!(
1710            doc.copy_at(0, &pointer("/one"), &pointer("/two"))
1711                .unwrap_err()
1712                .to_string()
1713                .contains("anchor")
1714        );
1715        assert!(
1716            doc.test_at(0, &pointer("/one/value"), &fragment("1.0"))
1717                .unwrap()
1718        );
1719    }
1720}