blob: 3c8e3610a8b153cf7409335a4aed24e33f186c2d [file] [log] [blame]
// Copyright 2020 Garett Tok Ern Liang
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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];
final 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 represnetation 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;
}
final end = getContentSensitiveEnd(currValue);
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);
/// 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;
final end = getContentSensitiveEnd(nodeToRemove);
return SourceEdit(start, end - start, '[]');
}
final yaml = yamlEdit.toString();
final span = nodeToRemove.span;
/// The general removal strategy is to remove everything that starts from
/// [nodeToRemove]'s dash to the next node's dash.
///
/// -1 accounts for the fact that the content can start with a dash
var start = yaml.lastIndexOf('-', span.start.offset - 1);
var end = yaml.lastIndexOf('\n', list.span.end.offset) + 1;
if (index < list.length - 1) {
final nextNode = list.nodes[index + 1];
end = yaml.lastIndexOf('-', nextNode.span.start.offset - 1);
} else {
/// If there is a possibility that there is a `-` or `\n` 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;
} 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, '');
}