| // 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 'dart:async'; |
| |
| import 'package:analysis_server/src/protocol_server.dart' |
| hide Element, ElementKind; |
| import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart'; |
| import 'package:analysis_server/src/services/completion/dart/utilities.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/src/util/comment.dart'; |
| |
| /** |
| * Determine the number of arguments. |
| */ |
| int _argCount(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| if (node is ArgumentList) { |
| if (request.target.entity == node.rightParenthesis) { |
| // Parser ignores trailing commas |
| if (node.findPrevious(node.rightParenthesis)?.lexeme == ',') { |
| return node.arguments.length + 1; |
| } |
| } |
| return node.arguments.length; |
| } |
| return 0; |
| } |
| |
| /** |
| * Determine if the completion target is at the end of the list of arguments. |
| */ |
| bool _isAppendingToArgList(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| if (node is ArgumentList) { |
| var entity = request.target.entity; |
| if (entity == node.rightParenthesis) { |
| return true; |
| } |
| if (node.arguments.isNotEmpty && node.arguments.last == entity) { |
| return entity is SimpleIdentifier; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Determine if the completion target is the label for a named argument. |
| */ |
| bool _isEditingNamedArgLabel(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| if (node is ArgumentList) { |
| var entity = request.target.entity; |
| if (entity is NamedExpression) { |
| int offset = request.offset; |
| if (entity.offset < offset && offset < entity.end) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return `true` if the [request] is inside of a [NamedExpression] name. |
| */ |
| bool _isInNamedExpression(DartCompletionRequest request) { |
| Object entity = request.target.entity; |
| if (entity is NamedExpression) { |
| Label name = entity.name; |
| return name.offset < request.offset && request.offset < name.end; |
| } |
| return false; |
| } |
| |
| /** |
| * Determine if the completion target is in the middle or beginning of the list |
| * of named parameters and is not preceded by a comma. This method assumes that |
| * _isAppendingToArgList has been called and is false. |
| */ |
| bool _isInsertingToArgListWithNoSynthetic(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| if (node is ArgumentList) { |
| var entity = request.target.entity; |
| return entity is NamedExpression; |
| } |
| return false; |
| } |
| |
| /** |
| * Determine 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 |
| * _isAppendingToArgList and _isInsertingToArgListWithNoSynthetic have been |
| * called and both return false. |
| */ |
| bool _isInsertingToArgListWithSynthetic(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| if (node is ArgumentList) { |
| var entity = request.target.entity; |
| if (entity is SimpleIdentifier) { |
| int 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 (node.arguments.length == argIndex + 1 || |
| node.arguments.getRange(argIndex + 1, argIndex + 2).first |
| is NamedExpression) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return a collection of currently specified named arguments |
| */ |
| Iterable<String> _namedArgs(DartCompletionRequest request) { |
| AstNode node = request.target.containingNode; |
| List<String> namedArgs = new List<String>(); |
| if (node is ArgumentList) { |
| for (Expression arg in node.arguments) { |
| if (arg is NamedExpression) { |
| namedArgs.add(arg.name.label.name); |
| } |
| } |
| } |
| return namedArgs; |
| } |
| |
| /** |
| * A contributor for calculating `completion.getSuggestions` request results |
| * when the cursor position is inside the arguments to a method call. |
| */ |
| class ArgListContributor extends DartCompletionContributor { |
| DartCompletionRequest request; |
| List<CompletionSuggestion> suggestions; |
| |
| @override |
| Future<List<CompletionSuggestion>> computeSuggestions( |
| DartCompletionRequest request) async { |
| // TODO(brianwilkerson) Determine whether this await is necessary. |
| await null; |
| this.request = request; |
| this.suggestions = <CompletionSuggestion>[]; |
| |
| var executable = request.target.executableElement; |
| if (executable == null) { |
| return const <CompletionSuggestion>[]; |
| } |
| |
| _addSuggestions(executable.parameters); |
| return suggestions; |
| } |
| |
| void _addDefaultParamSuggestions(Iterable<ParameterElement> parameters, |
| [bool appendComma = false]) { |
| bool appendColon = !_isInNamedExpression(request); |
| Iterable<String> namedArgs = _namedArgs(request); |
| for (ParameterElement parameter in parameters) { |
| if (parameter.isNamed) { |
| _addNamedParameterSuggestion( |
| namedArgs, parameter, appendColon, appendComma); |
| } |
| } |
| } |
| |
| void _addNamedParameterSuggestion(List<String> namedArgs, |
| ParameterElement parameter, bool appendColon, bool appendComma) { |
| String name = parameter.name; |
| String type = parameter.type?.displayName; |
| if (name != null && name.isNotEmpty && !namedArgs.contains(name)) { |
| String completion = name; |
| if (appendColon) { |
| completion += ': '; |
| } |
| int selectionOffset = completion.length; |
| |
| // Optionally add Flutter child widget details. |
| Element element = parameter.enclosingElement; |
| if (element is ConstructorElement) { |
| var flutter = Flutter.of(request.result); |
| if (flutter.isWidget(element.enclosingElement)) { |
| String value = getDefaultStringParameterValue(parameter); |
| if (value == '<Widget>[]') { |
| completion += value; |
| selectionOffset = completion.length - 1; // before closing ']' |
| } |
| } |
| } |
| |
| if (appendComma) { |
| completion += ','; |
| } |
| |
| final int relevance = parameter.hasRequired |
| ? DART_RELEVANCE_NAMED_PARAMETER_REQUIRED |
| : DART_RELEVANCE_NAMED_PARAMETER; |
| |
| CompletionSuggestion suggestion = new CompletionSuggestion( |
| CompletionSuggestionKind.NAMED_ARGUMENT, |
| relevance, |
| completion, |
| selectionOffset, |
| 0, |
| false, |
| false, |
| parameterName: name, |
| parameterType: type); |
| if (parameter is FieldFormalParameterElement) { |
| _setDocumentation(suggestion, parameter.field?.documentationComment); |
| suggestion.element = convertElement(parameter); |
| } |
| |
| suggestions.add(suggestion); |
| } |
| } |
| |
| void _addSuggestions(Iterable<ParameterElement> parameters) { |
| if (parameters == null || parameters.isEmpty) { |
| return; |
| } |
| Iterable<ParameterElement> requiredParam = |
| parameters.where((ParameterElement p) => p.isRequiredPositional); |
| int requiredCount = requiredParam.length; |
| // 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(request) || _isAppendingToArgList(request)) { |
| if (requiredCount == 0 || requiredCount < _argCount(request)) { |
| bool addTrailingComma = |
| !_isFollowedByAComma(request) && _isInFlutterCreation(request); |
| _addDefaultParamSuggestions(parameters, addTrailingComma); |
| } |
| } else if (_isInsertingToArgListWithNoSynthetic(request)) { |
| _addDefaultParamSuggestions(parameters, true); |
| } else if (_isInsertingToArgListWithSynthetic(request)) { |
| _addDefaultParamSuggestions(parameters, !_isFollowedByAComma(request)); |
| } |
| } |
| |
| bool _isFollowedByAComma(DartCompletionRequest request) { |
| // 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; |
| Token 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(DartCompletionRequest request) { |
| var flutter = Flutter.of(request.result); |
| AstNode containingNode = request?.target?.containingNode; |
| InstanceCreationExpression newExpr = containingNode != null |
| ? flutter.identifyNewExpression(containingNode.parent) |
| : null; |
| return newExpr != null && flutter.isWidgetCreation(newExpr); |
| } |
| |
| /** |
| * If the given [comment] is not `null`, fill the [suggestion] documentation |
| * fields. |
| */ |
| static void _setDocumentation( |
| CompletionSuggestion suggestion, String comment) { |
| if (comment != null) { |
| String doc = getDartDocPlainText(comment); |
| suggestion.docComplete = doc; |
| suggestion.docSummary = getDartDocSummary(doc); |
| } |
| } |
| } |