blob: 271b7fb588d66c57f4b8fd8febabe147c4f23943 [file] [log] [blame]
// Copyright (c) 2014, 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:analysis_server/src/provisional/completion/completion_core.dart';
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/utilities/flutter.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
/// A contributor that produces suggestions for named expression labels that
/// correspond to named parameters when completing in argument lists.
class ArgListContributor extends DartCompletionContributor {
/// The request that is currently being handled.
DartCompletionRequest request;
/// The suggestion builder used to build suggestions.
SuggestionBuilder builder;
/// The argument list that is the containing node of the target, or `null` if
/// the containing node of the target is not an argument list (such as when
/// it's a named expression).
ArgumentList argumentList;
@override
Future<void> computeSuggestions(
DartCompletionRequest request, SuggestionBuilder builder) async {
var parameters = request.target.executableElement?.parameters ??
request.target.functionType?.parameters;
if (parameters == null) {
return;
}
var node = request.target.containingNode;
if (node is ArgumentList) {
argumentList = node;
}
this.request = request;
this.builder = builder;
_addSuggestions(parameters);
}
void _addDefaultParamSuggestions(Iterable<ParameterElement> parameters,
{bool appendComma = false, int replacementLength}) {
var appendColon = !_isInNamedExpression();
var namedArgs = _namedArgs();
for (var parameter in parameters) {
if (parameter.isNamed) {
_addNamedParameterSuggestion(
namedArgs, parameter, appendColon, appendComma,
replacementLength: replacementLength);
}
}
}
void _addNamedParameterSuggestion(List<String> namedArgs,
ParameterElement parameter, bool appendColon, bool appendComma,
{int replacementLength}) {
var name = parameter.name;
// Check whether anything after the caret is being replaced. If so, we will
// suppress inserting colons/commas. We check only replacing _after_ the
// caret as some replacements (before) will still want colons, for example:
// foo(mySt^'bar');
var replacementEnd = request.replacementRange.offset +
(replacementLength ?? request.replacementRange.length);
var willReplace =
request.completionPreference == CompletionPreference.replace &&
replacementEnd > request.offset;
if (name != null && name.isNotEmpty && !namedArgs.contains(name)) {
builder.suggestNamedArgument(parameter,
// If there's a replacement length and the preference is to replace,
// we should not include colons/commas.
appendColon: appendColon && !willReplace,
appendComma: appendComma && !willReplace,
replacementLength: replacementLength);
}
}
void _addSuggestions(Iterable<ParameterElement> parameters) {
if (parameters == null || parameters.isEmpty) {
return;
}
var requiredParam =
parameters.where((ParameterElement p) => p.isRequiredPositional);
var requiredCount = requiredParam.length;
// When inserted named args, if there is a replacement starting at the caret
// it will be an identifier that should not be replaced if the completion
// preference is to insert. In this case, override the replacement length
// to 0.
// TODO(jwren): _isAppendingToArgList can be split into two cases (with and
// without preceded), then _isAppendingToArgList,
// _isInsertingToArgListWithNoSynthetic and
// _isInsertingToArgListWithSynthetic could be formatted into a single
// method which returns some enum with 5+ cases.
if (_isEditingNamedArgLabel() ||
_isAppendingToArgList() ||
_isAddingLabelToPositional()) {
if (requiredCount == 0 || requiredCount < _argCount()) {
// If there's a replacement range that starts at the caret, it will be
// for an identifier that is not the named label and therefore it should
// not be replaced.
var replacementLength =
request.offset == request.target.entity.offset &&
request.replacementRange.length != 0
? 0
: null;
var addTrailingComma = !_isFollowedByAComma() && _isInFlutterCreation();
_addDefaultParamSuggestions(parameters,
appendComma: addTrailingComma,
replacementLength: replacementLength);
}
} else if (_isInsertingToArgListWithNoSynthetic()) {
_addDefaultParamSuggestions(parameters, appendComma: true);
} else if (_isInsertingToArgListWithSynthetic()) {
_addDefaultParamSuggestions(parameters,
appendComma: !_isFollowedByAComma());
} else {
var argument = request.target.containingNode;
if (argument is NamedExpression) {
_buildClosureSuggestions(argument);
}
}
}
/// Return the number of arguments in the argument list.
int _argCount() {
if (argumentList != null) {
var paren = argumentList.rightParenthesis;
if (request.target.entity == paren) {
// Parser ignores trailing commas
if (argumentList.findPrevious(paren)?.lexeme == ',') {
return argumentList.arguments.length + 1;
}
}
return argumentList.arguments.length;
}
return 0;
}
void _buildClosureSuggestions(NamedExpression argument) {
var type = argument.staticParameterElement?.type;
if (type is FunctionType) {
builder.suggestClosure(type,
includeTrailingComma:
argument.endToken.next?.type != TokenType.COMMA);
}
}
/// Return `true` if the caret is preceeding an arg where a name could be added
/// (turning a positional arg into a named arg).
bool _isAddingLabelToPositional() {
if (argumentList != null) {
var entity = request.target.entity;
if (entity is! NamedExpression) {
// Caret is in front of a value
// f(one: 1, ^2);
if (request.offset <= entity.offset) {
return true;
}
// Caret is in the between two values that are not seperated by a comma.
// f(one: 1, tw^'foo');
// must be at least two and the target not last.
var args = argumentList.arguments;
if (args.length >= 2 && entity != args.last) {
var index = args.indexOf(entity);
if (index != -1) {
var next = args[index + 1];
// Check the two tokens are adjacent without any comma.
if (entity.end == next.offset) {
return true;
}
}
}
}
}
return false;
}
/// Return `true` if the completion target is at the end of the list of
/// arguments.
bool _isAppendingToArgList() {
if (argumentList != null) {
var entity = request.target.entity;
if (entity == argumentList.rightParenthesis) {
return true;
}
if (argumentList.arguments.isNotEmpty &&
argumentList.arguments.last == entity) {
return entity is SimpleIdentifier;
}
}
return false;
}
/// Return `true` if the completion target is the label for a named argument.
bool _isEditingNamedArgLabel() {
if (argumentList != null) {
var entity = request.target.entity;
if (entity is NamedExpression) {
var offset = request.offset;
if (entity.offset < offset && offset < entity.end) {
return true;
}
}
}
return false;
}
bool _isFollowedByAComma() {
// new A(^); NO
// new A(one: 1, ^); NO
// new A(^ , one: 1); YES
// new A(^), ... NO
var containingNode = request.target.containingNode;
var entity = request.target.entity;
var token = entity is AstNode
? entity.endToken
: entity is Token
? entity
: null;
return (token != containingNode?.endToken) &&
token?.next?.type == TokenType.COMMA &&
!token.next.isSynthetic;
}
bool _isInFlutterCreation() {
var flutter = Flutter.instance;
var containingNode = request.target?.containingNode;
var newExpr = containingNode != null
? flutter.identifyNewExpression(containingNode.parent)
: null;
return newExpr != null && flutter.isWidgetCreation(newExpr);
}
/// Return `true` if the [request] is inside of a [NamedExpression] name.
bool _isInNamedExpression() {
var entity = request.target.entity;
if (entity is NamedExpression) {
var name = entity.name;
return name.offset < request.offset && request.offset < name.end;
}
return false;
}
/// Return `true` if the completion target is in the middle or beginning of
/// the list of named arguments and is not preceded by a comma. This method
/// assumes that [_isAppendingToArgList] has been called and returned `false`.
bool _isInsertingToArgListWithNoSynthetic() {
if (argumentList != null) {
var entity = request.target.entity;
return entity is NamedExpression;
}
return false;
}
/// Return `true` if the completion target is in the middle or beginning of
/// the list of named parameters and is preceded by a comma. This method
/// assumes that both [_isAppendingToArgList] and
/// [_isInsertingToArgListWithNoSynthetic] have been called and both returned
/// `false`.
bool _isInsertingToArgListWithSynthetic() {
if (argumentList != null) {
var entity = request.target.entity;
if (entity is SimpleIdentifier) {
var argIndex = request.target.argIndex;
// if the next argument is a NamedExpression, then we are in the named
// parameter list, guard first against end of list
if (argumentList.arguments.length == argIndex + 1 ||
argumentList.arguments.getRange(argIndex + 1, argIndex + 2).first
is NamedExpression) {
return true;
}
}
}
return false;
}
/// Return a list containing the currently specified named arguments.
List<String> _namedArgs() {
var namedArgs = <String>[];
if (argumentList != null) {
for (var arg in argumentList.arguments) {
if (arg is NamedExpression) {
namedArgs.add(arg.name.label.name);
}
}
}
return namedArgs;
}
}