Vendor yaml_edit (#2633)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 4839545..aabd67f 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:pub_semver/pub_semver.dart';
-import 'package:yaml_edit/yaml_edit.dart';
 
 import '../command.dart';
 import '../entrypoint.dart';
@@ -16,6 +15,7 @@
 import '../pubspec.dart';
 import '../solver.dart';
 import '../utils.dart';
+import '../yaml_edit/editor.dart';
 
 /// Handles the `add` pub command. Adds a dependency to `pubspec.yaml` and gets
 /// the package. The user may pass in a git constraint, host url, or path as
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index dc3bd26..94091c7 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -2,8 +2,6 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'package:yaml_edit/yaml_edit.dart';
-
 import '../command.dart';
 import '../entrypoint.dart';
 import '../io.dart';
@@ -11,6 +9,7 @@
 import '../package.dart';
 import '../pubspec.dart';
 import '../solver.dart';
+import '../yaml_edit/editor.dart';
 
 /// Handles the `remove` pub command. Removes dependencies from `pubspec.yaml`,
 /// and performs an operation similar to `pub get`. Unlike `pub add`, this
diff --git a/lib/src/yaml_edit/editor.dart b/lib/src/yaml_edit/editor.dart
new file mode 100644
index 0000000..fa397ce
--- /dev/null
+++ b/lib/src/yaml_edit/editor.dart
@@ -0,0 +1,632 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'errors.dart';
+import 'list_mutations.dart';
+import 'map_mutations.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// An interface for modififying [YAML][1] documents while preserving comments
+/// and whitespaces.
+///
+/// YAML parsing is supported by `package:yaml`, and modifications are performed
+/// as string operations. An error will be thrown if internal assertions fail -
+/// such a situation should be extremely rare, and should only occur with
+/// degenerate formatting.
+///
+/// Most modification methods require the user to pass in an [Iterable<Object>]
+/// path that holds the keys/indices to navigate to the element.
+///
+/// **Example:**
+/// ```yaml
+/// a: 1
+/// b: 2
+/// c:
+///   - 3
+///   - 4
+///   - {e: 5, f: [6, 7]}
+/// ```
+///
+/// To get to `7`, our path will be `['c', 2, 'f', 1]`. The path for the base
+/// object is the empty array `[]`. All modification methods will throw a
+/// [Argu,emtError] if the path provided is invalid. Note also that that the order
+/// of elements in the path is important, and it should be arranged in order of
+/// calling, with the first element being the first key or index to be called.
+///
+/// In most modification methods, users are required to pass in a value to be
+/// used for updating the YAML tree. This value is only allowed to either be a
+/// valid scalar that is recognizable by YAML (i.e. `bool`, `String`, `List`,
+/// `Map`, `num`, `null`) or a [YamlNode]. Should the user want to specify
+/// the style to be applied to the value passed in, the user may wrap the value
+/// using [wrapAsYamlNode] while passing in the appropriate `scalarStyle` or
+/// `collectionStyle`. While we try to respect the style that is passed in,
+/// there will be instances where the formatting will not result in valid YAML,
+/// and as such we will fallback to a default formatting while preserving the
+/// content.
+///
+/// To dump the YAML after all the modifications have been completed, simply
+/// call [toString()].
+///
+/// [1]: https://yaml.org/
+@sealed
+class YamlEditor {
+  final List<SourceEdit> _edits = [];
+
+  /// List of [SourceEdit]s that have been applied to [_yaml] since the creation
+  /// of this instance, in chronological order. Intended to be compatible with
+  /// `package:analysis_server`.
+  ///
+  /// The [SourceEdit] objects can be serialized to JSON using the `toJSON`
+  /// function, deserialized using [SourceEdit.fromJSON], and applied to a
+  /// string using the `apply` function. Multiple [SourceEdit]s can be applied
+  /// to a string using [SourceEdit.applyAll].
+  ///
+  /// For more information, refer to the [SourceEdit] class.
+  List<SourceEdit> get edits => [..._edits];
+
+  /// Current YAML string.
+  String _yaml;
+
+  /// Root node of YAML AST.
+  YamlNode _contents;
+
+  /// Stores the list of nodes in [_contents] that are connected by aliases.
+  ///
+  /// When a node is anchored with an alias and subsequently referenced,
+  /// the full content of the anchored node is thought to be copied in the
+  /// following references.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// a: &SS Sammy Sosa
+  /// b: *SS
+  /// ```
+  ///
+  /// is equivalent to
+  ///
+  /// ```dart
+  /// a: Sammy Sosa
+  /// b: Sammy Sosa
+  /// ```
+  ///
+  /// As such, aliased nodes have to be treated with special caution when
+  /// any modification is taking place.
+  ///
+  /// See 7.1 Alias Nodes: https://yaml.org/spec/1.2/spec.html#id2786196
+  Set<YamlNode> _aliases = {};
+
+  /// Returns the current YAML string.
+  @override
+  String toString() => _yaml;
+
+  factory YamlEditor(String yaml) => YamlEditor._(yaml);
+
+  YamlEditor._(this._yaml) {
+    ArgumentError.checkNotNull(_yaml);
+    _initialize();
+  }
+
+  /// Loads [_contents] from [_yaml], and traverses the YAML tree formed to
+  /// detect alias nodes.
+  void _initialize() {
+    _contents = loadYamlNode(_yaml);
+    _aliases = {};
+
+    /// Performs a DFS on [_contents] to detect alias nodes.
+    final visited = <YamlNode>{};
+    void collectAliases(YamlNode node) {
+      if (visited.add(node)) {
+        if (node is YamlMap) {
+          node.nodes.forEach((key, value) {
+            collectAliases(key);
+            collectAliases(value);
+          });
+        } else if (node is YamlList) {
+          node.nodes.forEach(collectAliases);
+        }
+      } else {
+        _aliases.add(node);
+      }
+    }
+
+    collectAliases(_contents);
+  }
+
+  /// Parses the document to return [YamlNode] currently present at [path].
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [ArgumentError].
+  ///
+  /// To get `null` when [path] does not point to a value in the [YamlNode]-tree,
+  /// simply pass `orElse: () => null`.
+  ///
+  /// **Example:** (using orElse)
+  /// ```dart
+  /// final myYamlEditor('{"key": "value"}');
+  /// final value = myYamlEditor.valueAt(['invalid', 'path'], orElse: () => null);
+  /// print(value) // null
+  /// ```
+  ///
+  /// **Example:** (common usage)
+  /// ```dart
+  ///   final doc = YamlEditor('''
+  /// a: 1
+  /// b:
+  ///   d: 4
+  ///   e: [5, 6, 7]
+  /// c: 3
+  /// ''');
+  /// print(doc.parseAt(['b', 'e', 2])); // 7
+  /// ```
+  /// The value returned by [parseAt] is invalidated when the documented is
+  /// mutated, as illustrated below:
+  ///
+  /// **Example:** (old [parseAt] value is invalidated)
+  /// ```dart
+  /// final doc = YamlEditor("YAML: YAML Ain't Markup Language");
+  /// final node = doc.parseAt(['YAML']);
+  ///
+  /// print(node.value); // Expected output: "YAML Ain't Markup Language"
+  ///
+  /// doc.update(['YAML'], 'YAML');
+  ///
+  /// final newNode = doc.parseAt(['YAML']);
+  ///
+  /// // Note that the value does not change
+  /// print(newNode.value); // "YAML"
+  /// print(node.value); // "YAML Ain't Markup Language"
+  /// ```
+  YamlNode parseAt(Iterable<Object> path, {YamlNode Function() orElse}) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    return _traverse(path, orElse: orElse);
+  }
+
+  /// Sets [value] in the [path].
+  ///
+  /// There is a subtle difference between [update] and [remove] followed by
+  /// an [insertIntoList], because [update] preserves comments at the same level.
+  ///
+  /// Throws a [ArgumentError] if [path] is invalid.
+  ///
+  /// **Example:** (using [update])
+  /// ```dart
+  /// final doc = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc.update([1], 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test # comment
+  ///   - 2
+  /// ```
+  ///
+  /// **Example:** (using [remove] and [insertIntoList])
+  /// ```dart
+  /// final doc2 = YamlEditor('''
+  ///   - 0
+  ///   - 1 # comment
+  ///   - 2
+  /// ''');
+  /// doc2.remove([1]);
+  /// doc2.insertIntoList([], 1, 'test');
+  /// ```
+  ///
+  /// **Expected Output:**
+  /// ```yaml
+  ///   - 0
+  ///   - test
+  ///   - 2
+  /// ```
+  void update(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    final valueNode = wrapAsYamlNode(value);
+
+    if (path.isEmpty) {
+      final start = _contents.span.start.offset;
+      final end = getContentSensitiveEnd(_contents);
+      final lineEnding = getLineEnding(_yaml);
+      final edit = SourceEdit(
+          start, end - start, yamlEncodeBlockString(valueNode, 0, lineEnding));
+
+      return _performEdit(edit, path, valueNode);
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath, checkAlias: true);
+
+    if (parentNode is YamlList) {
+      final expected = wrapAsYamlNode(
+        [...parentNode.nodes]..[keyOrIndex] = valueNode,
+      );
+
+      return _performEdit(updateInList(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expected);
+    }
+
+    if (parentNode is YamlMap) {
+      final expectedMap =
+          updatedYamlMap(parentNode, (nodes) => nodes[keyOrIndex] = valueNode);
+      return _performEdit(updateInMap(this, parentNode, keyOrIndex, valueNode),
+          collectionPath, expectedMap);
+    }
+
+    throw PathError.unexpected(
+        path, 'Scalar $parentNode does not have key $keyOrIndex');
+  }
+
+  /// Appends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 1]');
+  /// doc.appendToList([], 2); // [0, 1, 2]
+  /// ```
+  void appendToList(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+    final yamlList = _traverseToList(path);
+
+    insertIntoList(path, yamlList.length, value);
+  }
+
+  /// Prepends [value] to the list at [path].
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[1, 2]');
+  /// doc.prependToList([], 0); // [0, 1, 2]
+  /// ```
+  void prependToList(Iterable<Object> path, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    insertIntoList(path, 0, value);
+  }
+
+  /// Inserts [value] into the list at [path].
+  ///
+  /// [index] must be non-negative and no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[0, 2]');
+  /// doc.insertIntoList([], 1, 1); // [0, 1, 2]
+  /// ```
+  void insertIntoList(Iterable<Object> path, int index, Object value) {
+    ArgumentError.checkNotNull(path, 'path');
+    final valueNode = wrapAsYamlNode(value);
+
+    final list = _traverseToList(path, checkAlias: true);
+    RangeError.checkValueInInterval(index, 0, list.length);
+
+    final edit = insertInList(this, list, index, valueNode);
+    final expected = wrapAsYamlNode(
+      [...list.nodes]..insert(index, valueNode),
+    );
+
+    _performEdit(edit, path, expected);
+  }
+
+  /// Changes the contents of the list at [path] by removing [deleteCount] items
+  /// at [index], and inserting [values] in-place. Returns the elements that
+  /// are deleted.
+  ///
+  /// [index] and [deleteCount] must be non-negative and [index] + [deleteCount]
+  /// must be no greater than the list's length.
+  ///
+  /// Throws a [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('[Jan, March, April, June]');
+  /// doc.spliceList([], 1, 0, ['Feb']); // [Jan, Feb, March, April, June]
+  /// doc.spliceList([], 4, 1, ['May']); // [Jan, Feb, March, April, May]
+  /// ```
+  Iterable<YamlNode> spliceList(Iterable<Object> path, int index,
+      int deleteCount, Iterable<Object> values) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(index, 'index');
+    ArgumentError.checkNotNull(deleteCount, 'deleteCount');
+    ArgumentError.checkNotNull(values, 'values');
+
+    final list = _traverseToList(path, checkAlias: true);
+
+    RangeError.checkValueInInterval(index, 0, list.length);
+    RangeError.checkValueInInterval(index + deleteCount, 0, list.length);
+
+    final nodesToRemove = list.nodes.getRange(index, index + deleteCount);
+
+    /// Perform addition of elements before removal to avoid scenarioes where
+    /// a block list gets emptied out to {} to avoid changing collection styles
+    /// where possible.
+
+    /// Reverse [values] and insert them.
+    final reversedValues = values.toList().reversed;
+    for (final value in reversedValues) {
+      insertIntoList(path, index, value);
+    }
+
+    for (var i = 0; i < deleteCount; i++) {
+      remove([...path, index + values.length]);
+    }
+
+    return nodesToRemove;
+  }
+
+  /// Removes the node at [path]. Comments "belonging" to the node will be
+  /// removed while surrounding comments will be left untouched.
+  ///
+  /// Throws a [ArgumentError] if [path] is invalid.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final doc = YamlEditor('''
+  /// - 0 # comment 0
+  /// # comment A
+  /// - 1 # comment 1
+  /// # comment B
+  /// - 2 # comment 2
+  /// ''');
+  /// doc.remove([1]);
+  /// ```
+  ///
+  /// **Expected Result:**
+  /// ```dart
+  /// '''
+  /// - 0 # comment 0
+  /// # comment A
+  /// # comment B
+  /// - 2 # comment 2
+  /// '''
+  /// ```
+  YamlNode remove(Iterable<Object> path) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    SourceEdit edit;
+    YamlNode expectedNode;
+    final nodeToRemove = _traverse(path, checkAlias: true);
+
+    if (path.isEmpty) {
+      edit = SourceEdit(0, _yaml.length, '');
+
+      /// Parsing an empty YAML document returns `null`.
+      _performEdit(edit, path, expectedNode);
+      return nodeToRemove;
+    }
+
+    final pathAsList = path.toList();
+    final collectionPath = pathAsList.take(path.length - 1);
+    final keyOrIndex = pathAsList.last;
+    final parentNode = _traverse(collectionPath);
+
+    if (parentNode is YamlList) {
+      edit = removeInList(this, parentNode, keyOrIndex);
+      expectedNode = wrapAsYamlNode(
+        [...parentNode.nodes]..removeAt(keyOrIndex),
+      );
+    } else if (parentNode is YamlMap) {
+      edit = removeInMap(this, parentNode, keyOrIndex);
+
+      expectedNode =
+          updatedYamlMap(parentNode, (nodes) => nodes.remove(keyOrIndex));
+    }
+
+    _performEdit(edit, collectionPath, expectedNode);
+
+    return nodeToRemove;
+  }
+
+  /// Traverses down [path] to return the [YamlNode] at [path] if successful.
+  ///
+  /// If no [YamlNode]s exist at [path], the result of invoking the [orElse]
+  /// function is returned.
+  ///
+  /// If [orElse] is omitted, it defaults to throwing a [PathError].
+  ///
+  /// If [checkAlias] is `true`, throw [AliasError] if an aliased node is
+  /// encountered.
+  YamlNode _traverse(Iterable<Object> path,
+      {bool checkAlias = false, YamlNode Function() orElse}) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(checkAlias, 'checkAlias');
+
+    if (path.isEmpty) return _contents;
+
+    var currentNode = _contents;
+    final pathList = path.toList();
+
+    for (var i = 0; i < pathList.length; i++) {
+      final keyOrIndex = pathList[i];
+
+      if (checkAlias && _aliases.contains(currentNode)) {
+        throw AliasError(path, currentNode);
+      }
+
+      if (currentNode is YamlList) {
+        final list = currentNode as YamlList;
+        if (!isValidIndex(keyOrIndex, list.length)) {
+          return _pathErrorOrElse(path, path.take(i + 1), list, orElse);
+        }
+
+        currentNode = list.nodes[keyOrIndex];
+      } else if (currentNode is YamlMap) {
+        final map = currentNode as YamlMap;
+
+        if (!containsKey(map, keyOrIndex)) {
+          return _pathErrorOrElse(path, path.take(i + 1), map, orElse);
+        }
+        final keyNode = getKeyNode(map, keyOrIndex);
+
+        if (checkAlias) {
+          if (_aliases.contains(keyNode)) throw AliasError(path, keyNode);
+        }
+
+        currentNode = map.nodes[keyNode];
+      } else {
+        return _pathErrorOrElse(path, path.take(i + 1), currentNode, orElse);
+      }
+    }
+
+    if (checkAlias) _assertNoChildAlias(path, currentNode);
+
+    return currentNode;
+  }
+
+  /// Throws a [PathError] if [orElse] is not provided, returns the result
+  /// of invoking the [orElse] function otherwise.
+  YamlNode _pathErrorOrElse(Iterable<Object> path, Iterable<Object> subPath,
+      YamlNode parent, YamlNode Function() orElse) {
+    if (orElse == null) throw PathError(path, subPath, parent);
+    return orElse();
+  }
+
+  /// Asserts that [node] and none its children are aliases
+  void _assertNoChildAlias(Iterable<Object> path, [YamlNode node]) {
+    ArgumentError.checkNotNull(path, 'path');
+
+    if (node == null) return _assertNoChildAlias(path, _traverse(path));
+    if (_aliases.contains(node)) throw AliasError(path, node);
+
+    if (node is YamlScalar) return;
+
+    if (node is YamlList) {
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, i];
+        _assertNoChildAlias(updatedPath, node.nodes[i]);
+      }
+    }
+
+    if (node is YamlMap) {
+      final keyList = node.keys.toList();
+      for (var i = 0; i < node.length; i++) {
+        final updatedPath = [...path, keyList[i]];
+        if (_aliases.contains(keyList[i])) throw AliasError(path, keyList[i]);
+        _assertNoChildAlias(updatedPath, node.nodes[keyList[i]]);
+      }
+    }
+  }
+
+  /// Traverses down the provided [path] to return the [YamlList] at [path].
+  ///
+  /// Convenience function to ensure that a [YamlList] is returned.
+  ///
+  /// Throws [ArgumentError] if the element at the given path is not a [YamlList]
+  /// or if the path is invalid. If [checkAlias] is `true`, and an aliased node
+  /// is encountered along [path], an [AliasError] will be thrown.
+  YamlList _traverseToList(Iterable<Object> path, {bool checkAlias = false}) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(checkAlias, 'checkAlias');
+
+    final possibleList = _traverse(path, checkAlias: true);
+
+    if (possibleList is YamlList) {
+      return possibleList;
+    } else {
+      throw PathError.unexpected(
+          path, 'Path $path does not point to a YamlList!');
+    }
+  }
+
+  /// Utility method to replace the substring of [_yaml] according to [edit].
+  ///
+  /// When [_yaml] is modified with this method, the resulting string is parsed
+  /// and reloaded and traversed down [path] to ensure that the reloaded YAML
+  /// tree is equal to our expectations by deep equality of values. Throws an
+  /// [AssertionError] if the two trees do not match.
+  void _performEdit(
+      SourceEdit edit, Iterable<Object> path, YamlNode expectedNode) {
+    ArgumentError.checkNotNull(edit, 'edit');
+    ArgumentError.checkNotNull(path, 'path');
+
+    final expectedTree = _deepModify(_contents, path, [], expectedNode);
+    _yaml = edit.apply(_yaml);
+    _initialize();
+
+    final actualTree = loadYamlNode(_yaml);
+    if (!deepEquals(actualTree, expectedTree)) {
+      throw AssertionError('''
+Modification did not result in expected result! 
+
+Obtained: 
+$actualTree
+
+Expected: 
+$expectedTree''');
+    }
+
+    _contents = actualTree;
+    _edits.add(edit);
+  }
+
+  /// Utility method to produce an updated YAML tree equivalent to converting
+  /// the [YamlNode] at [path] to be [expectedNode]. [subPath] holds the portion
+  /// of [path] that has been traversed thus far.
+  ///
+  /// Throws a [PathError] if path is invalid.
+  ///
+  /// When called, it creates a new [YamlNode] of the same type as [tree], and
+  /// copies its children over, except for the child that is on the path. Doing
+  /// so allows us to "update" the immutable [YamlNode] without having to clone
+  /// the whole tree.
+  ///
+  /// [SourceSpan]s in this new tree are not guaranteed to be accurate.
+  YamlNode _deepModify(YamlNode tree, Iterable<Object> path,
+      Iterable<Object> subPath, YamlNode expectedNode) {
+    ArgumentError.checkNotNull(path, 'path');
+    ArgumentError.checkNotNull(tree, 'tree');
+    RangeError.checkValueInInterval(subPath.length, 0, path.length);
+
+    if (path.length == subPath.length) return expectedNode;
+
+    final keyOrIndex = path.elementAt(subPath.length);
+
+    if (tree is YamlList) {
+      if (!isValidIndex(keyOrIndex, tree.length)) {
+        throw PathError(path, subPath, tree);
+      }
+
+      return wrapAsYamlNode([...tree.nodes]..[keyOrIndex] = _deepModify(
+          tree.nodes[keyOrIndex],
+          path,
+          path.take(subPath.length + 1),
+          expectedNode));
+    }
+
+    if (tree is YamlMap) {
+      return updatedYamlMap(
+          tree,
+          (nodes) => nodes[keyOrIndex] = _deepModify(nodes[keyOrIndex], path,
+              path.take(subPath.length + 1), expectedNode));
+    }
+
+    /// Should not ever reach here.
+    throw PathError(path, subPath, tree);
+  }
+}
diff --git a/lib/src/yaml_edit/equality.dart b/lib/src/yaml_edit/equality.dart
new file mode 100644
index 0000000..21ad6a0
--- /dev/null
+++ b/lib/src/yaml_edit/equality.dart
@@ -0,0 +1,116 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+import 'package:yaml/yaml.dart';
+
+/// Creates a map that uses our custom [deepEquals] and [deepHashCode] functions
+/// to determine equality.
+Map<K, V> deepEqualsMap<K, V>() =>
+    LinkedHashMap(equals: deepEquals, hashCode: deepHashCode);
+
+/// Compares two [Object]s for deep equality. This implementation differs from
+/// `package:yaml`'s deep equality notation by allowing for comparison of
+/// non-scalar map keys.
+bool deepEquals(dynamic obj1, dynamic obj2) {
+  if (obj1 is YamlNode) obj1 = obj1.value;
+  if (obj2 is YamlNode) obj2 = obj2.value;
+
+  if (obj1 is Map && obj2 is Map) {
+    return mapDeepEquals(obj1, obj2);
+  }
+
+  if (obj1 is List && obj2 is List) {
+    return listDeepEquals(obj1, obj2);
+  }
+
+  return obj1 == obj2;
+}
+
+/// Compares two [List]s for deep equality.
+bool listDeepEquals(List list1, List list2) {
+  if (list1.length != list2.length) return false;
+
+  if (list1 is YamlList) list1 = (list1 as YamlList).nodes;
+  if (list2 is YamlList) list2 = (list2 as YamlList).nodes;
+
+  for (var i = 0; i < list1.length; i++) {
+    if (!deepEquals(list1[i], list2[i])) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+/// Compares two [Map]s for deep equality. Differs from `package:yaml`'s deep
+/// equality notation by allowing for comparison of non-scalar map keys.
+bool mapDeepEquals(Map map1, Map map2) {
+  if (map1.length != map2.length) return false;
+
+  if (map1 is YamlList) map1 = (map1 as YamlMap).nodes;
+  if (map2 is YamlList) map2 = (map2 as YamlMap).nodes;
+
+  return map1.keys.every((key) {
+    if (!containsKey(map2, key)) return false;
+
+    /// Because two keys may be equal by deep equality but using one key on the
+    /// other map might not get a hit since they may not be both using our
+    /// [deepEqualsMap].
+    final key2 = getKey(map2, key);
+
+    if (!deepEquals(map1[key], map2[key2])) {
+      return false;
+    }
+
+    return true;
+  });
+}
+
+/// Returns a hashcode for [value] such that structures that are equal by
+/// [deepEquals] will have the same hash code.
+int deepHashCode(Object value) {
+  if (value is Map) {
+    const equality = UnorderedIterableEquality();
+    return equality.hash(value.keys.map(deepHashCode)) ^
+        equality.hash(value.values.map(deepHashCode));
+  } else if (value is Iterable) {
+    return const IterableEquality().hash(value.map(deepHashCode));
+  } else if (value is YamlScalar) {
+    return value.value.hashCode;
+  }
+
+  return value.hashCode;
+}
+
+/// Returns the [YamlNode] corresponding to the provided [key].
+YamlNode getKeyNode(YamlMap map, Object key) {
+  return map.nodes.keys.firstWhere((node) => deepEquals(node, key)) as YamlNode;
+}
+
+/// Returns the [YamlNode] after the [YamlNode] corresponding to the provided
+/// [key].
+YamlNode getNextKeyNode(YamlMap map, Object key) {
+  final keyIterator = map.nodes.keys.iterator;
+  while (keyIterator.moveNext()) {
+    if (deepEquals(keyIterator.current, key) && keyIterator.moveNext()) {
+      return keyIterator.current;
+    }
+  }
+
+  return null;
+}
+
+/// Returns the key in [map] that is equal to the provided [key] by the notion
+/// of deep equality.
+Object getKey(Map map, Object key) {
+  return map.keys.firstWhere((k) => deepEquals(k, key));
+}
+
+/// Checks if [map] has any keys equal to the provided [key] by deep equality.
+bool containsKey(Map map, Object key) {
+  return map.keys.where((node) => deepEquals(node, key)).isNotEmpty;
+}
diff --git a/lib/src/yaml_edit/errors.dart b/lib/src/yaml_edit/errors.dart
new file mode 100644
index 0000000..5d1c9a0
--- /dev/null
+++ b/lib/src/yaml_edit/errors.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+import 'package:yaml/yaml.dart';
+
+/// Error thrown when a function is passed an invalid path.
+@sealed
+class PathError extends ArgumentError {
+  /// The full path that caused the error
+  final Iterable<Object> path;
+
+  /// The subpath that caused the error
+  final Iterable<Object> subPath;
+
+  /// The last element of [path] that could be traversed.
+  YamlNode parent;
+
+  PathError(this.path, this.subPath, this.parent, [String message])
+      : super.value(subPath, 'path', message);
+
+  PathError.unexpected(this.path, String message)
+      : subPath = path,
+        super(message);
+
+  @override
+  String toString() {
+    if (message == null) {
+      var errorMessage = 'Failed to traverse to subpath $subPath!';
+
+      if (subPath.isNotEmpty) {
+        errorMessage +=
+            ' Parent $parent does not contain key or index ${subPath.last}';
+      }
+
+      return 'Invalid path: $path. $errorMessage.';
+    }
+
+    return 'Invalid path: $path. $message';
+  }
+}
+
+/// Error thrown when the path contains an alias along the way.
+///
+/// When a path contains an aliased node, the behavior becomes less well-defined
+/// because we cannot be certain if the user wishes for the change to
+/// propagate throughout all the other aliased nodes, or if the user wishes
+/// for only that particular node to be modified. As such, [AliasError] reflects
+/// the detection that our change will impact an alias, and we do not intend
+/// on supporting such changes for the foreseeable future.
+@sealed
+class AliasError extends UnsupportedError {
+  /// The path that caused the error
+  final Iterable<Object> path;
+
+  /// The anchor node of the alias
+  final YamlNode anchor;
+
+  AliasError(this.path, this.anchor)
+      : super('Encountered an alias node along $path! '
+            'Alias nodes are nodes that refer to a previously serialized nodes, '
+            'and are denoted by either the "*" or the "&" indicators in the '
+            'original YAML. As the resulting behavior of mutations on these '
+            'nodes is not well-defined, the operation will not be supported '
+            'by this library.\n\n'
+            '${anchor.span.message('The alias was first defined here.')}');
+}
diff --git a/lib/src/yaml_edit/list_mutations.dart b/lib/src/yaml_edit/list_mutations.dart
new file mode 100644
index 0000000..9eaa949
--- /dev/null
+++ b/lib/src/yaml_edit/list_mutations.dart
@@ -0,0 +1,335 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of setting the element at [index] to [newValue] when re-parsed.
+SourceEdit updateInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final currValue = list.nodes[index];
+  var offset = currValue.span.start.offset;
+  final yaml = yamlEdit.toString();
+  String valueString;
+
+  /// We do not use [_formatNewBlock] since we want to only replace the contents
+  /// of this node while preserving comments/whitespace, while [_formatNewBlock]
+  /// produces a string representation of a new node.
+  if (list.style == CollectionStyle.BLOCK) {
+    final listIndentation = getListIndentation(yaml, list);
+    final indentation = listIndentation + getIndentation(yamlEdit);
+    final lineEnding = getLineEnding(yaml);
+    valueString = yamlEncodeBlockString(
+        wrapAsYamlNode(newValue), indentation, lineEnding);
+
+    /// We prefer the compact nested notation for collections.
+    ///
+    /// By virtue of [yamlEncodeBlockString], collections automatically
+    /// have the necessary line endings.
+    if ((newValue is List && (newValue as List).isNotEmpty) ||
+        (newValue is Map && (newValue as Map).isNotEmpty)) {
+      valueString = valueString.substring(indentation);
+    } else if (isCollection(currValue) &&
+        getStyle(currValue) == CollectionStyle.BLOCK) {
+      valueString += lineEnding;
+    }
+
+    var end = getContentSensitiveEnd(currValue);
+    if (end <= offset) {
+      offset++;
+      end = offset;
+      valueString = ' ' + valueString;
+    }
+
+    return SourceEdit(offset, end - offset, valueString);
+  } else {
+    valueString = yamlEncodeFlowString(newValue);
+    return SourceEdit(offset, currValue.span.length, valueString);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of appending [item] to the list.
+SourceEdit appendIntoList(YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  if (list.style == CollectionStyle.FLOW) {
+    return _appendToFlowList(yamlEdit, list, item);
+  } else {
+    return _appendToBlockList(yamlEdit, list, item);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] to the list at [index].
+SourceEdit insertInList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  /// We call the append method if the user wants to append it to the end of the
+  /// list because appending requires different techniques.
+  if (index == list.length) {
+    return appendIntoList(yamlEdit, list, item);
+  } else {
+    if (list.style == CollectionStyle.FLOW) {
+      return _insertInFlowList(yamlEdit, list, index, item);
+    } else {
+      return _insertInBlockList(yamlEdit, list, index, item);
+    }
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing the element at [index] when re-parsed.
+SourceEdit removeInList(YamlEditor yamlEdit, YamlList list, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final nodeToRemove = list.nodes[index];
+
+  if (list.style == CollectionStyle.FLOW) {
+    return _removeFromFlowList(yamlEdit, list, nodeToRemove, index);
+  } else {
+    return _removeFromBlockList(yamlEdit, list, nodeToRemove, index);
+  }
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of addition [item] into [nodes], noting that this is a flow list.
+SourceEdit _appendToFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final valueString = _formatNewFlow(list, item, true);
+  return SourceEdit(list.span.end.offset - 1, 0, valueString);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of addition [item] into [nodes], noting that this is a block list.
+SourceEdit _appendToBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  var formattedValue = _formatNewBlock(yamlEdit, list, item);
+  final yaml = yamlEdit.toString();
+  var offset = list.span.end.offset;
+
+  // Adjusts offset to after the trailing newline of the last entry, if it exists
+  if (list.isNotEmpty) {
+    final lastValueSpanEnd = list.nodes.last.span.end.offset;
+    final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd);
+    if (nextNewLineIndex == -1) {
+      formattedValue = getLineEnding(yaml) + formattedValue;
+    } else {
+      offset = nextNewLineIndex + 1;
+    }
+  }
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Formats [item] into a new node for block lists.
+String _formatNewBlock(YamlEditor yamlEdit, YamlList list, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+
+  final yaml = yamlEdit.toString();
+  final listIndentation = getListIndentation(yaml, list);
+  final newIndentation = listIndentation + getIndentation(yamlEdit);
+  final lineEnding = getLineEnding(yaml);
+
+  var valueString = yamlEncodeBlockString(item, newIndentation, lineEnding);
+  if (isCollection(item) && !isFlowYamlCollectionNode(item) && !isEmpty(item)) {
+    valueString = valueString.substring(newIndentation);
+  }
+  final indentedHyphen = ' ' * listIndentation + '- ';
+
+  return '$indentedHyphen$valueString$lineEnding';
+}
+
+/// Formats [item] into a new node for flow lists.
+String _formatNewFlow(YamlList list, YamlNode item, [bool isLast = false]) {
+  ArgumentError.checkNotNull(list, 'list');
+  ArgumentError.checkNotNull(isLast, 'isLast');
+
+  var valueString = yamlEncodeFlowString(item);
+  if (list.isNotEmpty) {
+    if (isLast) {
+      valueString = ', $valueString';
+    } else {
+      valueString += ', ';
+    }
+  }
+
+  return valueString;
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] into [nodes] at [index], noting that this is
+/// a block list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _insertInBlockList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToBlockList(yamlEdit, list, item);
+
+  final formattedValue = _formatNewBlock(yamlEdit, list, item);
+
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  final yaml = yamlEdit.toString();
+  final start = yaml.lastIndexOf('\n', currNodeStart) + 1;
+
+  return SourceEdit(start, 0, formattedValue);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of inserting [item] into [nodes] at [index], noting that this is
+/// a flow list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _insertInFlowList(
+    YamlEditor yamlEdit, YamlList list, int index, YamlNode item) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length);
+
+  if (index == list.length) return _appendToFlowList(yamlEdit, list, item);
+
+  final formattedValue = _formatNewFlow(list, item);
+
+  final yaml = yamlEdit.toString();
+  final currNode = list.nodes[index];
+  final currNodeStart = currNode.span.start.offset;
+  var start = yaml.lastIndexOf(RegExp(r',|\['), currNodeStart - 1) + 1;
+  if (yaml[start] == ' ') start++;
+
+  return SourceEdit(start, 0, formattedValue);
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing [nodeToRemove] from [nodes], noting that this is a
+/// block list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _removeFromBlockList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  var end = getContentSensitiveEnd(nodeToRemove);
+
+  /// If we are removing the last element in a block list, convert it into a
+  /// flow empty list.
+  if (list.length == 1) {
+    final start = list.span.start.offset;
+
+    return SourceEdit(start, end - start, '[]');
+  }
+
+  final yaml = yamlEdit.toString();
+  final span = nodeToRemove.span;
+
+  /// Adjust the end to clear the new line after the end too.
+  ///
+  /// We do this because we suspect that our users will want the inline
+  /// comments to disappear too.
+  final nextNewLine = yaml.indexOf('\n', end);
+  if (nextNewLine != -1) {
+    end = nextNewLine + 1;
+  }
+
+  /// If the value is empty
+  if (span.length == 0) {
+    var start = span.start.offset;
+    return SourceEdit(start, end - start, '');
+  }
+
+  /// -1 accounts for the fact that the content can start with a dash
+  var start = yaml.lastIndexOf('-', span.start.offset - 1);
+
+  /// Check if there is a `-` before the node
+  if (start > 0) {
+    final lastHyphen = yaml.lastIndexOf('-', start - 1);
+    final lastNewLine = yaml.lastIndexOf('\n', start - 1);
+    if (lastHyphen > lastNewLine) {
+      start = lastHyphen + 2;
+
+      /// If there is a `-` before the node, we need to check if we have
+      /// to update the indentation of the next node.
+      if (index < list.length - 1) {
+        /// Since [end] is currently set to the next new line after the current
+        /// node, check if we see a possible comment first, or a hyphen first.
+        /// Note that no actual content can appear here.
+        ///
+        /// We check this way because the start of a span in a block list is
+        /// the start of its value, and checking from the back leaves us
+        /// easily confused if there are comments that have dashes in them.
+        final nextHash = yaml.indexOf('#', end);
+        final nextHyphen = yaml.indexOf('-', end);
+        final nextNewLine = yaml.indexOf('\n', end);
+
+        /// If [end] is on the same line as the hyphen of the next node
+        if ((nextHash == -1 || nextHyphen < nextHash) &&
+            nextHyphen < nextNewLine) {
+          end = nextHyphen;
+        }
+      }
+    } else if (lastNewLine > lastHyphen) {
+      start = lastNewLine + 1;
+    }
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Returns a [SourceEdit] describing the change to be made on [yaml] to achieve
+/// the effect of removing [nodeToRemove] from [nodes], noting that this is a
+/// flow list.
+///
+/// [index] should be non-negative and less than or equal to [length].
+SourceEdit _removeFromFlowList(
+    YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(list, 'list');
+  RangeError.checkValueInInterval(index, 0, list.length - 1);
+
+  final span = nodeToRemove.span;
+  final yaml = yamlEdit.toString();
+  var start = span.start.offset;
+  var end = span.end.offset;
+
+  if (index == 0) {
+    start = yaml.lastIndexOf('[', start - 1) + 1;
+    if (index == list.length - 1) {
+      end = yaml.indexOf(']', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/lib/src/yaml_edit/map_mutations.dart b/lib/src/yaml_edit/map_mutations.dart
new file mode 100644
index 0000000..3dd8682
--- /dev/null
+++ b/lib/src/yaml_edit/map_mutations.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+import 'equality.dart';
+import 'source_edit.dart';
+import 'strings.dart';
+import 'utils.dart';
+import 'wrap.dart';
+
+/// Performs the string operation on [yaml] to achieve the effect of setting
+/// the element at [key] to [newValue] when re-parsed.
+SourceEdit updateInMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (!containsKey(map, key)) {
+    final keyNode = wrapAsYamlNode(key);
+
+    if (map.style == CollectionStyle.FLOW) {
+      return _addToFlowMap(yamlEdit, map, keyNode, newValue);
+    } else {
+      return _addToBlockMap(yamlEdit, map, keyNode, newValue);
+    }
+  } else {
+    if (map.style == CollectionStyle.FLOW) {
+      return _replaceInFlowMap(yamlEdit, map, key, newValue);
+    } else {
+      return _replaceInBlockMap(yamlEdit, map, key, newValue);
+    }
+  }
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the element at [key] when re-parsed.
+SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object key) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (!containsKey(map, key)) return null;
+
+  final keyNode = getKeyNode(map, key);
+  final valueNode = map.nodes[keyNode];
+
+  if (map.style == CollectionStyle.FLOW) {
+    return _removeFromFlowMap(yamlEdit, map, keyNode, valueNode);
+  } else {
+    return _removeFromBlockMap(yamlEdit, map, keyNode, valueNode);
+  }
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of adding
+/// the [key]:[newValue] pair when reparsed, bearing in mind that this is a
+/// block map.
+SourceEdit _addToBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final yaml = yamlEdit.toString();
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+  final keyString = yamlEncodeFlowString(wrapAsYamlNode(key));
+  final lineEnding = getLineEnding(yaml);
+
+  var valueString = yamlEncodeBlockString(newValue, newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    valueString = '$lineEnding$valueString';
+  }
+
+  var formattedValue = ' ' * getMapIndentation(yaml, map) + '$keyString: ';
+  var offset = map.span.end.offset;
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (map.isNotEmpty) {
+    /// Adjusts offset to after the trailing newline of the last entry, if it
+    /// exists
+    if (insertionIndex == map.length) {
+      final lastValueSpanEnd = getContentSensitiveEnd(map.nodes.values.last);
+      final nextNewLineIndex = yaml.indexOf('\n', lastValueSpanEnd);
+
+      if (nextNewLineIndex != -1) {
+        offset = nextNewLineIndex + 1;
+      } else {
+        formattedValue = lineEnding + formattedValue;
+      }
+    } else {
+      final keyAtIndex = map.nodes.keys.toList()[insertionIndex] as YamlNode;
+      final keySpanStart = keyAtIndex.span.start.offset;
+      final prevNewLineIndex = yaml.lastIndexOf('\n', keySpanStart);
+
+      offset = prevNewLineIndex + 1;
+    }
+  }
+
+  formattedValue += valueString + lineEnding;
+
+  return SourceEdit(offset, 0, formattedValue);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of adding
+/// the [key]:[newValue] pair when reparsed, bearing in mind that this is a flow
+/// map.
+SourceEdit _addToFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final keyString = yamlEncodeFlowString(keyNode);
+  final valueString = yamlEncodeFlowString(newValue);
+
+  // The -1 accounts for the closing bracket.
+  if (map.isEmpty) {
+    return SourceEdit(map.span.end.offset - 1, 0, '$keyString: $valueString');
+  }
+
+  final insertionIndex = getMapInsertionIndex(map, keyString);
+
+  if (insertionIndex == map.length) {
+    return SourceEdit(map.span.end.offset - 1, 0, ', $keyString: $valueString');
+  }
+
+  final insertionOffset =
+      (map.nodes.keys.toList()[insertionIndex] as YamlNode).span.start.offset;
+
+  return SourceEdit(insertionOffset, 0, '$keyString: $valueString, ');
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of replacing
+/// the value at [key] with [newValue] when reparsed, bearing in mind that this
+/// is a block map.
+SourceEdit _replaceInBlockMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final yaml = yamlEdit.toString();
+  final lineEnding = getLineEnding(yaml);
+  final newIndentation =
+      getMapIndentation(yaml, map) + getIndentation(yamlEdit);
+
+  final keyNode = getKeyNode(map, key);
+  var valueAsString = yamlEncodeBlockString(
+      wrapAsYamlNode(newValue), newIndentation, lineEnding);
+  if (isCollection(newValue) &&
+      !isFlowYamlCollectionNode(newValue) &&
+      !isEmpty(newValue)) {
+    valueAsString = lineEnding + valueAsString;
+  }
+
+  /// +1 accounts for the colon
+  final start = keyNode.span.end.offset + 1;
+  var end = getContentSensitiveEnd(map.nodes[key]);
+
+  /// `package:yaml` parses empty nodes in a way where the start/end of the
+  /// empty value node is the end of the key node, so we have to adjust for
+  /// this.
+  if (end < start) end = start + 1;
+
+  return SourceEdit(start, end - start, ' ' + valueAsString);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of replacing
+/// the value at [key] with [newValue] when reparsed, bearing in mind that this
+/// is a flow map.
+SourceEdit _replaceInFlowMap(
+    YamlEditor yamlEdit, YamlMap map, Object key, YamlNode newValue) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+
+  final valueSpan = map.nodes[key].span;
+  final valueString = yamlEncodeFlowString(newValue);
+
+  return SourceEdit(valueSpan.start.offset, valueSpan.length, valueString);
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the [key] from the map, bearing in mind that this is a block map.
+SourceEdit _removeFromBlockMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+  ArgumentError.checkNotNull(keyNode, 'keyNode');
+  ArgumentError.checkNotNull(valueNode, 'valueNode');
+
+  final keySpan = keyNode.span;
+  var end = getContentSensitiveEnd(valueNode);
+  final yaml = yamlEdit.toString();
+
+  if (map.length == 1) {
+    final start = map.span.start.offset;
+    return SourceEdit(start, end - start, '{}');
+  }
+
+  var start = keySpan.start.offset;
+
+  /// Adjust the end to clear the new line after the end too.
+  ///
+  /// We do this because we suspect that our users will want the inline
+  /// comments to disappear too.
+  final nextNewLine = yaml.indexOf('\n', end);
+  if (nextNewLine != -1) {
+    end = nextNewLine + 1;
+  }
+
+  final nextNode = getNextKeyNode(map, keyNode);
+
+  if (start > 0) {
+    final lastHyphen = yaml.lastIndexOf('-', start - 1);
+    final lastNewLine = yaml.lastIndexOf('\n', start - 1);
+    if (lastHyphen > lastNewLine) {
+      start = lastHyphen + 2;
+
+      /// If there is a `-` before the node, and the end is on the same line
+      /// as the next node, we need to add the necessary offset to the end to
+      /// make sure the next node has the correct indentation.
+      if (nextNode != null &&
+          nextNode.span.start.offset - end <= nextNode.span.start.column) {
+        end += nextNode.span.start.column;
+      }
+    } else if (lastNewLine > lastHyphen) {
+      start = lastNewLine + 1;
+    }
+  }
+
+  return SourceEdit(start, end - start, '');
+}
+
+/// Performs the string operation on [yaml] to achieve the effect of removing
+/// the [key] from the map, bearing in mind that this is a flow map.
+SourceEdit _removeFromFlowMap(
+    YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode valueNode) {
+  ArgumentError.checkNotNull(yamlEdit, 'yamlEdit');
+  ArgumentError.checkNotNull(map, 'map');
+  ArgumentError.checkNotNull(keyNode, 'keyNode');
+  ArgumentError.checkNotNull(valueNode, 'valueNode');
+
+  var start = keyNode.span.start.offset;
+  var end = valueNode.span.end.offset;
+  final yaml = yamlEdit.toString();
+
+  if (deepEquals(keyNode, map.keys.first)) {
+    start = yaml.lastIndexOf('{', start - 1) + 1;
+
+    if (deepEquals(keyNode, map.keys.last)) {
+      end = yaml.indexOf('}', end);
+    } else {
+      end = yaml.indexOf(',', end) + 1;
+    }
+  } else {
+    start = yaml.lastIndexOf(',', start - 1);
+  }
+
+  return SourceEdit(start, end - start, '');
+}
diff --git a/lib/src/yaml_edit/source_edit.dart b/lib/src/yaml_edit/source_edit.dart
new file mode 100644
index 0000000..d52373b
--- /dev/null
+++ b/lib/src/yaml_edit/source_edit.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+
+/// A class representing a change on a [String], intended to be compatible with
+/// `package:analysis_server`'s [SourceEdit].
+///
+/// For example, changing a string from
+/// ```
+/// foo: foobar
+/// ```
+/// to
+/// ```
+/// foo: barbar
+/// ```
+/// will be represented by `SourceEdit(offset: 4, length: 3, replacement: 'bar')`
+@sealed
+class SourceEdit {
+  /// The offset from the start of the string where the modification begins.
+  final int offset;
+
+  /// The length of the substring to be replaced.
+  final int length;
+
+  /// The replacement string to be used.
+  final String replacement;
+
+  /// Creates a new [SourceEdit] instance. [offset], [length] and [replacement]
+  /// must be non-null, and [offset] and [length] must be non-negative.
+  factory SourceEdit(int offset, int length, String replacement) =>
+      SourceEdit._(offset, length, replacement);
+
+  SourceEdit._(this.offset, this.length, this.replacement) {
+    ArgumentError.checkNotNull(offset, 'offset');
+    ArgumentError.checkNotNull(length, 'length');
+    ArgumentError.checkNotNull(replacement, 'replacement');
+    RangeError.checkNotNegative(offset);
+    RangeError.checkNotNegative(length);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other is SourceEdit) {
+      return offset == other.offset &&
+          length == other.length &&
+          replacement == other.replacement;
+    }
+
+    return false;
+  }
+
+  @override
+  int get hashCode => offset.hashCode ^ length.hashCode ^ replacement.hashCode;
+
+  /// Constructs a SourceEdit from JSON.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = {
+  ///   'offset': 1,
+  ///   'length': 2,
+  ///   'replacement': 'replacement string'
+  /// };
+  ///
+  /// final sourceEdit = SourceEdit.fromJson(edit);
+  /// ```
+  factory SourceEdit.fromJson(Map<String, dynamic> json) {
+    ArgumentError.checkNotNull(json, 'json');
+
+    if (json is Map) {
+      final offset = json['offset'];
+      final length = json['length'];
+      final replacement = json['replacement'];
+
+      if (offset is int && length is int && replacement is String) {
+        return SourceEdit(offset, length, replacement);
+      }
+    }
+    throw FormatException('Invalid JSON passed to SourceEdit');
+  }
+
+  /// Encodes this object as JSON-compatible structure.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// import 'dart:convert' show jsonEncode;
+  ///
+  /// final edit = SourceEdit(offset, length, 'replacement string');
+  /// final jsonString = jsonEncode(edit.toJson());
+  /// print(jsonString);
+  /// ```
+  Map<String, dynamic> toJson() {
+    return {'offset': offset, 'length': length, 'replacement': replacement};
+  }
+
+  @override
+  String toString() => 'SourceEdit($offset, $length, "$replacement")';
+
+  /// Applies a series of [SourceEdit]s to an original string, and return the
+  /// final output.
+  ///
+  /// [edits] should be in order i.e. the first [SourceEdit] in [edits] should
+  /// be the first edit applied to [original].
+  ///
+  /// **Example:**
+  /// ```dart
+  /// const original = 'YAML: YAML';
+  /// final sourceEdits = [
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
+  ///        SourceEdit(0, 4, "YAML Ain't Markup Language")
+  ///      ];
+  /// final result = SourceEdit.applyAll(original, sourceEdits);
+  /// ```
+  /// **Expected result:**
+  /// ```dart
+  /// "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup
+  /// Language"
+  /// ```
+  static String applyAll(String original, Iterable<SourceEdit> edits) {
+    ArgumentError.checkNotNull(original, 'original');
+    ArgumentError.checkNotNull(edits, 'edits');
+
+    return edits.fold(original, (current, edit) => edit.apply(current));
+  }
+
+  /// Applies one [SourceEdit]s to an original string, and return the final
+  /// output.
+  ///
+  /// **Example:**
+  /// ```dart
+  /// final edit = SourceEdit(4, 3, 'bar');
+  /// final originalString = 'foo: foobar';
+  /// print(edit.apply(originalString)); // 'foo: barbar'
+  /// ```
+  String apply(String original) {
+    ArgumentError.checkNotNull(original, 'original');
+
+    return original.replaceRange(offset, offset + length, replacement);
+  }
+}
diff --git a/lib/src/yaml_edit/strings.dart b/lib/src/yaml_edit/strings.dart
new file mode 100644
index 0000000..faebe8a
--- /dev/null
+++ b/lib/src/yaml_edit/strings.dart
@@ -0,0 +1,311 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:yaml/yaml.dart';
+import 'utils.dart';
+
+/// Given [value], tries to format it into a plain string recognizable by YAML.
+/// If it fails, it defaults to returning a double-quoted string.
+///
+/// Not all values can be formatted into a plain string. If the string contains
+/// an escape sequence, it can only be detected when in a double-quoted
+/// sequence. Plain strings may also be misinterpreted by the YAML parser (e.g.
+/// ' null').
+String _tryYamlEncodePlain(Object value) {
+  if (value is YamlNode) {
+    AssertionError(
+        'YamlNodes should not be passed directly into getSafeString!');
+  }
+
+  assertValidScalar(value);
+
+  if (value is String) {
+    /// If it contains a dangerous character we want to wrap the result with
+    /// double quotes because the double quoted style allows for arbitrary
+    /// strings with "\" escape sequences.
+    ///
+    /// See 7.3.1 Double-Quoted Style
+    /// https://yaml.org/spec/1.2/spec.html#id2787109
+    if (isDangerousString(value)) {
+      return _yamlEncodeDoubleQuoted(value);
+    }
+
+    return value;
+  }
+
+  return value.toString();
+}
+
+/// Checks if [string] has unprintable characters according to
+/// [unprintableCharCodes].
+bool _hasUnprintableCharacters(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  final codeUnits = string.codeUnits;
+
+  for (final key in unprintableCharCodes.keys) {
+    if (codeUnits.contains(key)) return true;
+  }
+
+  return false;
+}
+
+/// Generates a YAML-safe double-quoted string based on [string], escaping the
+/// list of characters as defined by the YAML 1.2 spec.
+///
+/// See 5.7 Escaped Characters https://yaml.org/spec/1.2/spec.html#id2776092
+String _yamlEncodeDoubleQuoted(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  final buffer = StringBuffer();
+  for (final codeUnit in string.codeUnits) {
+    if (doubleQuoteEscapeChars[codeUnit] != null) {
+      buffer.write(doubleQuoteEscapeChars[codeUnit]);
+    } else {
+      buffer.writeCharCode(codeUnit);
+    }
+  }
+
+  return '"$buffer"';
+}
+
+/// Generates a YAML-safe single-quoted string. Automatically escapes
+/// single-quotes.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeSingleQuoted(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  final result = string.replaceAll('\'', '\'\'');
+  return '\'$result\'';
+}
+
+/// Generates a YAML-safe folded string.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(string, 'string');
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  String result;
+
+  final trimmedString = string.trimRight();
+  final removedPortion = string.substring(trimmedString.length);
+
+  if (removedPortion.contains('\n')) {
+    result = '>+\n' + ' ' * indentation;
+  } else {
+    result = '>-\n' + ' ' * indentation;
+  }
+
+  /// Duplicating the newline for folded strings preserves it in YAML.
+  /// Assumes the user did not try to account for windows documents by using
+  /// `\r\n` already
+  return result +
+      trimmedString.replaceAll('\n', lineEnding * 2 + ' ' * indentation) +
+      removedPortion;
+}
+
+/// Generates a YAML-safe literal string.
+///
+/// It is important that we ensure that [string] is free of unprintable
+/// characters by calling [assertValidScalar] before invoking this function.
+String _tryYamlEncodeLiteral(
+    String string, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(string, 'string');
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  final result = '|-\n$string';
+
+  /// Assumes the user did not try to account for windows documents by using
+  /// `\r\n` already
+  return result.replaceAll('\n', lineEnding + ' ' * indentation);
+}
+
+/// Returns [value] with the necessary formatting applied in a flow context
+/// if possible.
+///
+/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
+/// possible. Certain cases make this impossible (e.g. a plain string scalar that
+/// starts with '>'), in which case we will produce [value] with default styling
+/// options.
+String _yamlEncodeFlowScalar(YamlNode value) {
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+
+    if (value.value is String) {
+      if (_hasUnprintableCharacters(value.value) ||
+          value.style == ScalarStyle.DOUBLE_QUOTED) {
+        return _yamlEncodeDoubleQuoted(value.value);
+      }
+
+      if (value.style == ScalarStyle.SINGLE_QUOTED) {
+        return _tryYamlEncodeSingleQuoted(value.value);
+      }
+    }
+
+    return _tryYamlEncodePlain(value.value);
+  }
+
+  assertValidScalar(value);
+  return _tryYamlEncodePlain(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a block context
+/// if possible.
+///
+/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
+/// possible. Certain cases make this impossible (e.g. a folded string scalar
+/// 'null'), in which case we will produce [value] with default styling
+/// options.
+String yamlEncodeBlockScalar(
+    YamlNode value, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+
+    if (value.value is String) {
+      if (_hasUnprintableCharacters(value.value)) {
+        return _yamlEncodeDoubleQuoted(value.value);
+      }
+
+      if (value.style == ScalarStyle.SINGLE_QUOTED) {
+        return _tryYamlEncodeSingleQuoted(value.value);
+      }
+
+      // Strings with only white spaces will cause a misparsing
+      if (value.value.trim().length == value.value.length &&
+          value.value.length != 0) {
+        if (value.style == ScalarStyle.FOLDED) {
+          return _tryYamlEncodeFolded(value.value, indentation, lineEnding);
+        }
+
+        if (value.style == ScalarStyle.LITERAL) {
+          return _tryYamlEncodeLiteral(value.value, indentation, lineEnding);
+        }
+      }
+    }
+
+    return _tryYamlEncodePlain(value.value);
+  }
+
+  assertValidScalar(value);
+
+  /// The remainder of the possibilities are similar to how [getFlowScalar]
+  /// treats [value].
+  return _yamlEncodeFlowScalar(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a flow context.
+///
+/// If [value] is a [YamlNode], we try to respect its [style] parameter where
+/// possible. Certain cases make this impossible (e.g. a plain string scalar
+/// that starts with '>', a child having a block style parameters), in which
+/// case we will produce [value] with default styling options.
+String yamlEncodeFlowString(YamlNode value) {
+  if (value is YamlList) {
+    final list = value.nodes;
+
+    final safeValues = list.map(yamlEncodeFlowString);
+    return '[' + safeValues.join(', ') + ']';
+  } else if (value is YamlMap) {
+    final safeEntries = value.nodes.entries.map((entry) {
+      final safeKey = yamlEncodeFlowString(entry.key);
+      final safeValue = yamlEncodeFlowString(entry.value);
+      return '$safeKey: $safeValue';
+    });
+
+    return '{' + safeEntries.join(', ') + '}';
+  }
+
+  return _yamlEncodeFlowScalar(value);
+}
+
+/// Returns [value] with the necessary formatting applied in a block context.
+///
+/// If [value] is a [YamlNode], we respect its [style] parameter.
+String yamlEncodeBlockString(
+    YamlNode value, int indentation, String lineEnding) {
+  ArgumentError.checkNotNull(indentation, 'indentation');
+  ArgumentError.checkNotNull(lineEnding, 'lineEnding');
+
+  const additionalIndentation = 2;
+
+  if (!isBlockNode(value)) return yamlEncodeFlowString(value);
+
+  final newIndentation = indentation + additionalIndentation;
+
+  if (value is YamlList) {
+    if (value.isEmpty) return ' ' * indentation + '[]';
+
+    Iterable<String> safeValues;
+
+    final children = value.nodes;
+
+    safeValues = children.map((child) {
+      var valueString =
+          yamlEncodeBlockString(child, newIndentation, lineEnding);
+      if (isCollection(child) && !isFlowYamlCollectionNode(child)) {
+        valueString = valueString.substring(newIndentation);
+      }
+
+      return ' ' * indentation + '- $valueString';
+    });
+
+    return safeValues.join(lineEnding);
+  } else if (value is YamlMap) {
+    if (value.isEmpty) return ' ' * indentation + '{}';
+
+    return value.nodes.entries.map((entry) {
+      final safeKey = yamlEncodeFlowString(entry.key);
+      final formattedKey = ' ' * indentation + safeKey;
+      final formattedValue =
+          yamlEncodeBlockString(entry.value, newIndentation, lineEnding);
+
+      if (isCollection(entry.value)) {
+        return formattedKey + ':\n' + formattedValue;
+      }
+
+      return formattedKey + ': ' + formattedValue;
+    }).join(lineEnding);
+  }
+
+  return yamlEncodeBlockScalar(value, newIndentation, lineEnding);
+}
+
+/// List of unprintable characters.
+///
+/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
+final Map<int, String> unprintableCharCodes = {
+  0: '\\0', //  Escaped ASCII null (#x0) character.
+  7: '\\a', //  Escaped ASCII bell (#x7) character.
+  8: '\\b', //  Escaped ASCII backspace (#x8) character.
+  11: '\\v', // 	Escaped ASCII vertical tab (#xB) character.
+  12: '\\f', //  Escaped ASCII form feed (#xC) character.
+  13: '\\r', //  Escaped ASCII carriage return (#xD) character. Line Break.
+  27: '\\e', //  Escaped ASCII escape (#x1B) character.
+  133: '\\N', //  Escaped Unicode next line (#x85) character.
+  160: '\\_', //  Escaped Unicode non-breaking space (#xA0) character.
+  8232: '\\L', //  Escaped Unicode line separator (#x2028) character.
+  8233: '\\P', //  Escaped Unicode paragraph separator (#x2029) character.
+};
+
+/// List of escape characters. In particular, \x32 is not included because it
+/// can be processed normally.
+///
+/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
+final Map<int, String> doubleQuoteEscapeChars = {
+  ...unprintableCharCodes,
+  9: '\\t', //  Escaped ASCII horizontal tab (#x9) character. Printable
+  10: '\\n', //  Escaped ASCII line feed (#xA) character. Line Break.
+  34: '\\"', //  Escaped ASCII double quote (#x22).
+  47: '\\/', //  Escaped ASCII slash (#x2F), for JSON compatibility.
+  92: '\\\\', //  Escaped ASCII back slash (#x5C).
+};
diff --git a/lib/src/yaml_edit/utils.dart b/lib/src/yaml_edit/utils.dart
new file mode 100644
index 0000000..b4e64e6
--- /dev/null
+++ b/lib/src/yaml_edit/utils.dart
@@ -0,0 +1,269 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'editor.dart';
+
+/// Determines if [string] is dangerous by checking if parsing the plain string can
+/// return a result different from [string].
+///
+/// This function is also capable of detecting if non-printable characters are in
+/// [string].
+bool isDangerousString(String string) {
+  ArgumentError.checkNotNull(string, 'string');
+
+  try {
+    if (loadYamlNode(string).value != string) {
+      return true;
+    }
+
+    /// [string] should also not contain the `[`, `]`, `,`, `{` and `}` indicator characters.
+    return string.contains(RegExp(r'\{|\[|\]|\}|,'));
+  } catch (e) {
+    /// This catch statement catches [ArgumentError] in `loadYamlNode` when
+    /// a string can be interpreted as a URI tag, but catches for other
+    /// [YamlException]s
+    return true;
+  }
+}
+
+/// Asserts that [value] is a valid scalar according to YAML.
+///
+/// A valid scalar is a number, String, boolean, or null.
+void assertValidScalar(Object value) {
+  if (value is num || value is String || value is bool || value == null) {
+    return;
+  }
+
+  throw ArgumentError.value(value, 'value', 'Not a valid scalar type!');
+}
+
+/// Checks if [node] is a [YamlNode] with block styling.
+///
+/// [ScalarStyle.ANY] and [CollectionStyle.ANY] are considered to be block styling
+/// by default for maximum flexibility.
+bool isBlockNode(YamlNode node) {
+  ArgumentError.checkNotNull(node, 'node');
+
+  if (node is YamlScalar) {
+    if (node.style == ScalarStyle.LITERAL ||
+        node.style == ScalarStyle.FOLDED ||
+        node.style == ScalarStyle.ANY) {
+      return true;
+    }
+  }
+
+  if (node is YamlList &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) return true;
+  if (node is YamlMap &&
+      (node.style == CollectionStyle.BLOCK ||
+          node.style == CollectionStyle.ANY)) return true;
+
+  return false;
+}
+
+/// Returns the content sensitive ending offset of [yamlNode] (i.e. where the last
+/// meaningful content happens)
+int getContentSensitiveEnd(YamlNode yamlNode) {
+  ArgumentError.checkNotNull(yamlNode, 'yamlNode');
+
+  if (yamlNode is YamlList) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.last);
+    }
+  } else if (yamlNode is YamlMap) {
+    if (yamlNode.style == CollectionStyle.FLOW) {
+      return yamlNode.span.end.offset;
+    } else {
+      return getContentSensitiveEnd(yamlNode.nodes.values.last);
+    }
+  }
+
+  return yamlNode.span.end.offset;
+}
+
+/// Checks if the item is a Map or a List
+bool isCollection(Object item) => item is Map || item is List;
+
+/// Checks if [index] is [int], >=0, < [length]
+bool isValidIndex(Object index, int length) {
+  return index is int && index >= 0 && index < length;
+}
+
+/// Checks if the item is empty, if it is a List or a Map.
+///
+/// Returns `false` if [item] is not a List or Map.
+bool isEmpty(Object item) {
+  if (item is Map) return item.isEmpty;
+  if (item is List) return item.isEmpty;
+
+  return false;
+}
+
+/// Creates a [SourceSpan] from [sourceUrl] with no meaningful location
+/// information.
+///
+/// Mainly used with [wrapAsYamlNode] to allow for a reasonable
+/// implementation of [SourceSpan.message].
+SourceSpan shellSpan(Object sourceUrl) {
+  final shellSourceLocation = SourceLocation(0, sourceUrl: sourceUrl);
+  return SourceSpanBase(shellSourceLocation, shellSourceLocation, '');
+}
+
+/// Returns if [value] is a [YamlList] or [YamlMap] with [CollectionStyle.FLOW].
+bool isFlowYamlCollectionNode(Object value) {
+  if (value is YamlList || value is YamlMap) {
+    return (value as dynamic).style == CollectionStyle.FLOW;
+  }
+
+  return false;
+}
+
+/// Determines the index where [newKey] will be inserted if the keys in [map] are in
+/// alphabetical order when converted to strings.
+///
+/// Returns the length of [map] if the keys in [map] are not in alphabetical order.
+int getMapInsertionIndex(YamlMap map, Object newKey) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  final keys = map.nodes.keys.map((k) => k.toString()).toList();
+
+  for (var i = 1; i < keys.length; i++) {
+    if (keys[i].compareTo(keys[i - 1]) < 0) {
+      return map.length;
+    }
+  }
+
+  final insertionIndex = keys.indexWhere((key) => key.compareTo(newKey) > 0);
+
+  if (insertionIndex != -1) return insertionIndex;
+
+  return map.length;
+}
+
+/// Returns the [style] property of [target], if it is a [YamlNode]. Otherwise return null.
+Object getStyle(Object target) {
+  if (target is YamlNode) {
+    return (target as dynamic).style;
+  }
+
+  return null;
+}
+
+/// Returns the detected indentation step used in [yaml], or
+/// defaults to a value of `2` if no indentation step can be detected.
+///
+/// Indentation step is determined by the difference in indentation of the
+/// first block-styled yaml collection in the second level as compared to the
+/// top-level elements. In the case where there are multiple possible
+/// candidates, we choose the candidate closest to the start of [yaml].
+int getIndentation(YamlEditor editor) {
+  final node = editor.parseAt([]);
+  Iterable<YamlNode> children;
+  var indentation = 2;
+
+  if (node is YamlMap && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes.values;
+  } else if (node is YamlList && node.style == CollectionStyle.BLOCK) {
+    children = node.nodes;
+  }
+
+  if (children != null) {
+    for (final child in children) {
+      var indent = 0;
+      if (child is YamlList) {
+        indent = getListIndentation(editor.toString(), child);
+      } else if (child is YamlMap) {
+        indent = getMapIndentation(editor.toString(), child);
+      }
+
+      if (indent != 0) indentation = indent;
+    }
+  }
+  return indentation;
+}
+
+/// Gets the indentation level of [list]. This is 0 if it is a flow list,
+/// but returns the number of spaces before the hyphen of elements for
+/// block lists.
+///
+/// Throws [UnsupportedError] if an empty block map is passed in.
+int getListIndentation(String yaml, YamlList list) {
+  ArgumentError.checkNotNull(list, 'list');
+
+  if (list.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (list.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block list');
+  }
+
+  final lastSpanOffset = list.nodes.last.span.start.offset;
+  final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset - 1);
+  final lastHyphen = yaml.lastIndexOf('-', lastSpanOffset - 1);
+
+  if (lastNewLine == -1) return lastHyphen;
+
+  return lastHyphen - lastNewLine - 1;
+}
+
+/// Gets the indentation level of [map]. This is 0 if it is a flow map,
+/// but returns the number of spaces before the keys for block maps.
+int getMapIndentation(String yaml, YamlMap map) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  if (map.style == CollectionStyle.FLOW) return 0;
+
+  /// An empty block map doesn't really exist.
+  if (map.isEmpty) {
+    throw UnsupportedError('Unable to get indentation for empty block map');
+  }
+
+  /// Use the number of spaces between the last key and the newline as
+  /// indentation.
+  final lastKey = map.nodes.keys.last as YamlNode;
+  final lastSpanOffset = lastKey.span.start.offset;
+  final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset);
+  final lastQuestionMark = yaml.lastIndexOf('?', lastSpanOffset);
+
+  if (lastQuestionMark == -1) {
+    if (lastNewLine == -1) return lastSpanOffset;
+    return lastSpanOffset - lastNewLine - 1;
+  }
+
+  /// If there is a question mark, it might be a complex key. Check if it
+  /// is on the same line as the key node to verify.
+  if (lastNewLine == -1) return lastQuestionMark;
+  if (lastQuestionMark > lastNewLine) {
+    return lastQuestionMark - lastNewLine - 1;
+  }
+
+  return lastSpanOffset - lastNewLine - 1;
+}
+
+/// Returns the detected line ending used in [yaml], more specifically, whether
+/// [yaml] appears to use Windows `\r\n` or Unix `\n` line endings.
+///
+/// The heuristic used is to count all `\n` in the text and if stricly more
+/// than half of them are preceded by `\r` we report that windows line endings
+/// are used.
+String getLineEnding(String yaml) {
+  var index = -1;
+  var unixNewlines = 0;
+  var windowsNewlines = 0;
+  while ((index = yaml.indexOf('\n', index + 1)) != -1) {
+    if (index != 0 && yaml[index - 1] == '\r') {
+      windowsNewlines++;
+    } else {
+      unixNewlines++;
+    }
+  }
+
+  return windowsNewlines > unixNewlines ? '\r\n' : '\n';
+}
diff --git a/lib/src/yaml_edit/wrap.dart b/lib/src/yaml_edit/wrap.dart
new file mode 100644
index 0000000..5585ea6
--- /dev/null
+++ b/lib/src/yaml_edit/wrap.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection' as collection;
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import 'equality.dart';
+import 'utils.dart';
+
+/// Returns a new [YamlMap] constructed by applying [update] onto the [nodes]
+/// of this [YamlMap].
+YamlMap updatedYamlMap(YamlMap map, Function(Map) update) {
+  ArgumentError.checkNotNull(map, 'map');
+
+  final dummyMap = deepEqualsMap();
+  dummyMap.addAll(map.nodes);
+
+  update(dummyMap);
+
+  return wrapAsYamlNode(dummyMap);
+}
+
+/// Wraps [value] into a [YamlNode].
+///
+/// [Map]s, [List]s and Scalars will be wrapped as [YamlMap]s, [YamlList]s,
+/// and [YamlScalar]s respectively. If [collectionStyle]/[scalarStyle] is
+/// defined, and [value] is a collection or scalar, the wrapped [YamlNode] will
+/// have the respective style, otherwise it defaults to the ANY style.
+///
+/// If a [YamlNode] is passed in, no further wrapping will be done, and the
+/// [collectionStyle]/[scalarStyle] will not be applied.
+YamlNode wrapAsYamlNode(Object value,
+    {CollectionStyle collectionStyle = CollectionStyle.ANY,
+    ScalarStyle scalarStyle = ScalarStyle.ANY}) {
+  if (value is YamlScalar) {
+    assertValidScalar(value.value);
+    return value;
+  } else if (value is YamlList) {
+    for (final item in value.nodes) {
+      wrapAsYamlNode(item);
+    }
+
+    return value;
+  } else if (value is YamlMap) {
+    /// Both [entry.key] and [entry.values] are guaranteed to be [YamlNode]s,
+    /// so running this will just assert that they are valid scalars.
+    for (final entry in value.nodes.entries) {
+      wrapAsYamlNode(entry.key);
+      wrapAsYamlNode(entry.value);
+    }
+
+    return value;
+  } else if (value is Map) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+    return YamlMapWrap(value, collectionStyle: collectionStyle);
+  } else if (value is List) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+    return YamlListWrap(value, collectionStyle: collectionStyle);
+  } else {
+    assertValidScalar(value);
+
+    ArgumentError.checkNotNull(scalarStyle, 'scalarStyle');
+    return YamlScalarWrap(value, style: scalarStyle);
+  }
+}
+
+/// Internal class that allows us to define a constructor on [YamlScalar]
+/// which takes in [style] as an argument.
+class YamlScalarWrap implements YamlScalar {
+  /// The [ScalarStyle] to be used for the scalar.
+  @override
+  final ScalarStyle style;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  final dynamic value;
+
+  YamlScalarWrap(this.value, {this.style = ScalarStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl) {
+    ArgumentError.checkNotNull(style, 'scalarStyle');
+  }
+
+  @override
+  String toString() => value.toString();
+}
+
+/// Internal class that allows us to define a constructor on [YamlMap]
+/// which takes in [style] as an argument.
+class YamlMapWrap
+    with collection.MapMixin, UnmodifiableMapMixin
+    implements YamlMap {
+  /// The [CollectionStyle] to be used for the map.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final Map<dynamic, YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  factory YamlMapWrap(Map dartMap,
+      {CollectionStyle collectionStyle = CollectionStyle.ANY,
+      Object sourceUrl}) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+
+    final wrappedMap = deepEqualsMap<dynamic, YamlNode>();
+
+    for (final entry in dartMap.entries) {
+      final wrappedKey = wrapAsYamlNode(entry.key);
+      final wrappedValue = wrapAsYamlNode(entry.value);
+      wrappedMap[wrappedKey] = wrappedValue;
+    }
+
+    return YamlMapWrap._(wrappedMap,
+        style: collectionStyle, sourceUrl: sourceUrl);
+  }
+
+  YamlMapWrap._(this.nodes,
+      {CollectionStyle style = CollectionStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](Object key) => nodes[key]?.value;
+
+  @override
+  Iterable get keys => nodes.keys.map((node) => node.value);
+
+  @override
+  Map get value => this;
+}
+
+/// Internal class that allows us to define a constructor on [YamlList]
+/// which takes in [style] as an argument.
+class YamlListWrap with collection.ListMixin implements YamlList {
+  /// The [CollectionStyle] to be used for the list.
+  @override
+  final CollectionStyle style;
+
+  @override
+  final List<YamlNode> nodes;
+
+  @override
+  final SourceSpan span;
+
+  @override
+  int get length => nodes.length;
+
+  @override
+  set length(int index) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  factory YamlListWrap(List dartList,
+      {CollectionStyle collectionStyle = CollectionStyle.ANY,
+      Object sourceUrl}) {
+    ArgumentError.checkNotNull(collectionStyle, 'collectionStyle');
+
+    final wrappedList = dartList.map(wrapAsYamlNode).toList();
+    return YamlListWrap._(wrappedList,
+        style: collectionStyle, sourceUrl: sourceUrl);
+  }
+
+  YamlListWrap._(this.nodes,
+      {CollectionStyle style = CollectionStyle.ANY, Object sourceUrl})
+      : span = shellSpan(sourceUrl),
+        style = nodes.isEmpty ? CollectionStyle.FLOW : style;
+
+  @override
+  dynamic operator [](int index) => nodes[index].value;
+
+  @override
+  operator []=(int index, value) {
+    throw UnsupportedError('Cannot modify an unmodifiable List');
+  }
+
+  @override
+  List get value => this;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 076e303..30b464d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -28,7 +28,6 @@
   source_span: ^1.4.0
   stack_trace: ^1.0.0
   yaml: ^2.2.0
-  yaml_edit: ^1.0.1
 
 dev_dependencies:
   shelf_test_handler: ^1.0.0