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