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