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#[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 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 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 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 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 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 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 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 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}