blob: 04c27d0ee6584097df5d722a25a454d7fa8eed64 [file] [log] [blame]
// 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 [yamlEdit] 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) {
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 [yamlEdit] to achieve the effect of
/// removing the element at [key] when re-parsed.
SourceEdit removeInMap(YamlEditor yamlEdit, YamlMap map, Object? key) {
assert(containsKey(map, key));
if (map.style == CollectionStyle.FLOW) {
return _removeFromFlowMap(yamlEdit, map, key);
} else {
return _removeFromBlockMap(yamlEdit, map, key);
}
}
/// Performs the string operation on [yamlEdit] 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) {
final yaml = yamlEdit.toString();
final newIndentation =
getMapIndentation(yaml, map) + getIndentation(yamlEdit);
final keyString = yamlEncodeFlow(wrapAsYamlNode(key));
final lineEnding = getLineEnding(yaml);
var formattedValue = ' ' * getMapIndentation(yaml, map);
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;
}
}
var valueString = yamlEncodeBlock(newValue, newIndentation, lineEnding);
if (isCollection(newValue) &&
!isFlowYamlCollectionNode(newValue) &&
!isEmpty(newValue)) {
formattedValue += '$keyString:$lineEnding$valueString$lineEnding';
} else {
formattedValue += '$keyString: $valueString$lineEnding';
}
return SourceEdit(offset, 0, formattedValue);
}
/// Performs the string operation on [yamlEdit] to achieve the effect of adding
/// the [keyNode]:[newValue] pair when reparsed, bearing in mind that this is a
/// flow map.
SourceEdit _addToFlowMap(
YamlEditor yamlEdit, YamlMap map, YamlNode keyNode, YamlNode newValue) {
final keyString = yamlEncodeFlow(keyNode);
final valueString = yamlEncodeFlow(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 [yamlEdit] 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) {
final yaml = yamlEdit.toString();
final lineEnding = getLineEnding(yaml);
final newIndentation =
getMapIndentation(yaml, map) + getIndentation(yamlEdit);
final keyNode = getKeyNode(map, key);
var valueAsString =
yamlEncodeBlock(wrapAsYamlNode(newValue), newIndentation, lineEnding);
if (isCollection(newValue) &&
!isFlowYamlCollectionNode(newValue) &&
!isEmpty(newValue)) {
valueAsString = lineEnding + valueAsString;
}
if (!valueAsString.startsWith(lineEnding)) {
// prepend whitespace to ensure there is space after colon.
valueAsString = ' $valueAsString';
}
/// +1 accounts for the colon
// TODO: What if here is a whitespace following the key, before 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;
return SourceEdit(start, end - start, valueAsString);
}
/// Performs the string operation on [yamlEdit] 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) {
final valueSpan = map.nodes[key]!.span;
final valueString = yamlEncodeFlow(newValue);
return SourceEdit(valueSpan.start.offset, valueSpan.length, valueString);
}
/// Performs the string operation on [yamlEdit] 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, Object? key) {
final (index: entryIndex, :keyNode, :valueNode) = getYamlMapEntry(map, key);
final yaml = yamlEdit.toString();
final mapSize = map.length;
final keySpan = keyNode.span;
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 [key] from the map, bearing in mind that this is a flow
/// map.
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();
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, '');
}