Skip to content
14 changes: 14 additions & 0 deletions pkgs/yaml_edit/lib/src/equality.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ YamlNode getKeyNode(YamlMap map, Object? key) {
return map.nodes.keys.firstWhere((node) => deepEquals(node, key)) as YamlNode;
}

/// Returns the entry associated with a [mapKey] and its index in the [map].
({int index, YamlNode keyNode, YamlNode valueNode}) getYamlMapEntry(
YamlMap map,
Object? mapKey,
) {
for (final (index, MapEntry(:key, :value)) in map.nodes.entries.indexed) {
if (deepEquals(key, mapKey)) {
return (index: index, keyNode: key, valueNode: value);
}
}

throw YamlException('$mapKey not found in map', map.span);
}

/// Returns the [YamlNode] after the [YamlNode] corresponding to the provided
/// [key].
YamlNode? getNextKeyNode(YamlMap map, Object? key) {
Expand Down
100 changes: 38 additions & 62 deletions pkgs/yaml_edit/lib/src/list_mutations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,72 +306,48 @@ SourceEdit _insertInFlowList(
/// [index] should be non-negative and less than or equal to `list.length`.
SourceEdit _removeFromBlockList(
YamlEditor yamlEdit, YamlList list, YamlNode nodeToRemove, int index) {
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 listSize = list.length;
RangeError.checkValueInInterval(index, 0, listSize - 1);

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, '');
final isEmptySpan = span.length == 0; // Just the '-'
final end = getContentSensitiveEnd(nodeToRemove);

return removeBlockCollectionEntry(
yaml,
blockCollection: list,
collectionIndent: getListIndentation(yaml, list),
isFirstEntry: index == 0,
isSingleEntry: listSize == 1,
isLastEntry: index >= listSize - 1,
nodeToRemoveOffset: (
start: yaml.lastIndexOf(
'-',
isEmptySpan ? span.start.offset : span.start.offset - 1,
),
end: isEmptySpan ? end + 1 : end,
),
lineEnding: getLineEnding(yaml),
nextBlockNodeInfo: () {
final nextNode = list.nodes[index + 1];
final nextNodeSpan = nextNode.span;
final offset = nextNodeSpan.start.offset;

final hyphenOffset = yaml.lastIndexOf(
'-',
nextNodeSpan.length == 0 ? offset : offset - 1,
);

final nearestLineEnding = yaml.lastIndexOf('\n', hyphenOffset);

return (
nearestLineEnding: nearestLineEnding,
nextNodeColStart: hyphenOffset - (nearestLineEnding + 1),
);
},
);
}

/// Returns a [SourceEdit] describing the change to be made on [yamlEdit] to
Expand Down
103 changes: 38 additions & 65 deletions pkgs/yaml_edit/lib/src/map_mutations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@ SourceEdit updateInMap(
/// removing the element at [key] when re-parsed.
SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object? key) {
assert(containsKey(map, key));
final keyNode = getKeyNode(map, key);
final valueNode = map.nodes[keyNode]!;

if (map.style == CollectionStyle.FLOW) {
return _removeFromFlowMap(yamlEdit, map, keyNode, valueNode);
return _removeFromFlowMap(yamlEdit, map, key);
} else {
return _removeFromBlockMap(yamlEdit, map, keyNode, valueNode);
return _removeFromBlockMap(yamlEdit, map, key);
}
}

Expand Down Expand Up @@ -169,74 +167,49 @@ SourceEdit _replaceInFlowMap(
}

/// Performs the string operation on [yamlEdit] to achieve the effect of
/// removing the [keyNode] from the map, bearing in mind that this is a block
/// 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) {
final keySpan = keyNode.span;
var end = getContentSensitiveEnd(valueNode);
SourceEdit _removeFromBlockMap(YamlEditor yamlEdit, YamlMap map, Object? key) {
final (index: entryIndex, :keyNode, :valueNode) = getYamlMapEntry(map, key);
final yaml = yamlEdit.toString();
final lineEnding = getLineEnding(yaml);

if (map.length == 1) {
final start = map.span.start.offset;
final nextNewLine = yaml.indexOf(lineEnding, end);
if (nextNewLine != -1) {
// Remove everything up to the next newline, this strips comments that
// follows on the same line as the value we're removing.
// It also ensures we consume colon when [valueNode.value] is `null`
// because there is no value (e.g. `key: \n`). Because [valueNode.span] in
// such cases point to the colon `:`.
end = nextNewLine;
} else {
// Remove everything until the end of the document, if there is no newline
end = yaml.length;
}
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(lineEnding, end);
if (nextNewLine != -1) {
end = nextNewLine + lineEnding.length;
} else {
// Remove everything until the end of the document, if there is no newline
end = yaml.length;
}

final nextNode = getNextKeyNode(map, keyNode);

if (start > 0) {
final lastHyphen = yaml.lastIndexOf('-', start - 1);
final lastNewLine = yaml.lastIndexOf(lineEnding, 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 + lineEnding.length;
}
}
final mapSize = map.length;
final keySpan = keyNode.span;

return SourceEdit(start, end - start, '');
return removeBlockCollectionEntry(
yaml,
blockCollection: map,
collectionIndent: getMapIndentation(yaml, map),
isFirstEntry: entryIndex == 0,
isSingleEntry: mapSize == 1,
isLastEntry: entryIndex >= mapSize - 1,
nodeToRemoveOffset: (
// A block map only exists because of its first key.
start: entryIndex == 0 ? map.span.start.offset : keySpan.start.offset,
end: valueNode.span.length == 0
? keySpan.end.offset + 2 // Null value have no span. Skip ":".
: getContentSensitiveEnd(valueNode),
),
lineEnding: getLineEnding(yaml),

// Only called when the next node is present. Never before.
nextBlockNodeInfo: () {
final nextKeyNode = map.nodes.keys.elementAt(entryIndex + 1) as YamlNode;
final nextKeySpan = nextKeyNode.span.start;

return (
nearestLineEnding: yaml.lastIndexOf('\n', nextKeySpan.offset),
nextNodeColStart: nextKeySpan.column
);
},
);
}

/// Performs the string operation on [yamlEdit] to achieve the effect of
/// removing the [keyNode] from the map, bearing in mind that this is a flow
/// 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) {
SourceEdit _removeFromFlowMap(YamlEditor yamlEdit, YamlMap map, Object? key) {
final (index: _, :keyNode, :valueNode) = getYamlMapEntry(map, key);

var start = keyNode.span.start.offset;
var end = valueNode.span.end.offset;
final yaml = yamlEdit.toString();
Expand Down
Loading
Loading