blob: 2895eb43cdf547ddc05bb4979e410aa2bd2649a8 [file] [log] [blame]
// Copyright (c) 2017, 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.
/// A collection of utility methods used by completion contributors.
library;
import 'package:analysis_server/src/protocol_server.dart' show Location;
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
as protocol
show Element, ElementKind;
/// The name of the type `dynamic`;
const DYNAMIC = 'dynamic';
/// Sort by relevance first, highest to lowest, and then by the completion
/// alphabetically.
Comparator<CompletionSuggestionBuilder> completionComparator = (a, b) {
if (a.relevance == b.relevance) {
return a.completion.compareTo(b.completion);
}
return b.relevance.compareTo(a.relevance);
};
String buildClosureParameters(
FunctionType type, {
required bool includeTypes,
required bool includeKeywords,
}) {
var buffer = StringBuffer();
buffer.write('(');
var hasNamed = false;
var hasOptionalPositional = false;
var parameters = type.formalParameters;
var existingNames = parameters.map((p) => p.displayName).toSet();
for (var i = 0; i < parameters.length; ++i) {
var parameter = parameters[i];
if (i != 0) {
buffer.write(', ');
}
if (parameter.isNamed && !hasNamed) {
hasNamed = true;
buffer.write('{');
} else if (parameter.isOptionalPositional && !hasOptionalPositional) {
hasOptionalPositional = true;
buffer.write('[');
}
if (includeTypes) {
buffer.write(parameter.type);
buffer.write(' ');
}
var name = parameter.displayName;
if (name.isEmpty) {
name = 'p$i';
var index = 1;
while (existingNames.contains(name)) {
name = 'p${i}_$index';
index++;
}
}
if (includeKeywords && parameter.isRequiredNamed) {
buffer.write('required ');
}
buffer.write(name);
}
if (hasNamed) {
buffer.write('}');
} else if (hasOptionalPositional) {
buffer.write(']');
}
buffer.write(')');
return buffer.toString();
}
/// Compute default argument list text and ranges based on the given
/// [requiredParams] and [namedParams].
CompletionDefaultArgumentList computeCompletionDefaultArgumentList(
Element element,
Iterable<FormalParameterElement> requiredParams,
Iterable<FormalParameterElement> namedParams,
) {
var sb = StringBuffer();
var ranges = <int>[];
int offset;
for (var param in requiredParams) {
if (sb.isNotEmpty) {
sb.write(', ');
}
offset = sb.length;
var name = param.displayName;
sb.write(name);
ranges.addAll([offset, name.length]);
}
for (var param in namedParams) {
if (param.metadata2.hasRequired || param.isRequiredNamed) {
if (sb.isNotEmpty) {
sb.write(', ');
}
var name = param.displayName;
sb.write('$name: ');
offset = sb.length;
// TODO(pq): fix to use getDefaultStringParameterValue()
sb.write(name);
ranges.addAll([offset, name.length]);
}
}
return CompletionDefaultArgumentList(
text: sb.isNotEmpty ? sb.toString() : null,
ranges: ranges.isNotEmpty ? ranges : null,
);
}
/// Create a new protocol Element for inclusion in a completion suggestion.
protocol.Element createLocalElement(
Source source,
protocol.ElementKind kind,
SimpleIdentifier id, {
String? parameters,
TypeAnnotation? returnType,
bool isAbstract = false,
bool isDeprecated = false,
}) {
var name = id.name;
// TODO(danrubel): use lineInfo to determine startLine and startColumn
var location = Location(
source.fullName,
id.offset,
id.length,
0,
0,
endLine: 0,
endColumn: 0,
);
var flags = protocol.Element.makeFlags(
isAbstract: isAbstract,
isDeprecated: isDeprecated,
isPrivate: Identifier.isPrivateName(name),
);
return protocol.Element(
kind,
name,
flags,
location: location,
parameters: parameters,
returnType: nameForType(id, returnType),
);
}
/// Return a default argument value for the given [parameter].
DefaultArgument? getDefaultStringParameterValue(
FormalParameterElement parameter,
String quote,
) {
var type = parameter.type;
if (type is InterfaceType) {
if (type.isDartCoreList) {
return DefaultArgument('[]', cursorPosition: 1);
} else if (type.isDartCoreMap) {
return DefaultArgument('{}', cursorPosition: 1);
} else if (type.isDartCoreString) {
return DefaultArgument('$quote$quote', cursorPosition: 1);
}
} else if (type is FunctionType) {
var params = type.formalParameters.indexed
.map((r) {
var (index, parameter) = r;
var name = parameter.displayName.ifNotEmptyOrElse('p${index + 1}');
return '${getTypeString(parameter.type)}$name';
})
.join(', ');
// TODO(devoncarew): Support having this method return text with newlines.
var text = '($params) { }';
return DefaultArgument(text, cursorPosition: text.length - 2);
}
return null;
}
String getRequestLineIndent(DartCompletionRequest request) {
var content = request.content;
var lineStartOffset = request.offset;
var notWhitespaceOffset = request.offset;
for (; lineStartOffset > 0; lineStartOffset--) {
var char = content.substring(lineStartOffset - 1, lineStartOffset);
if (char == '\n') {
break;
}
if (char != ' ' && char != '\t') {
notWhitespaceOffset = lineStartOffset - 1;
}
}
return content.substring(lineStartOffset, notWhitespaceOffset);
}
String getTypeString(DartType type) {
if (type is DynamicType) {
return '';
} else {
return '${type.getDisplayString()} ';
}
}
/// Instantiates the given [InterfaceElement]
InterfaceType instantiateInstanceElement(
InterfaceElement element,
NeverType neverType,
) {
var typeParameters = element.typeParameters2;
var typeArguments = const <DartType>[];
if (typeParameters.isNotEmpty) {
typeArguments = List.filled(typeParameters.length, neverType);
}
return element.instantiate(
typeArguments: typeArguments,
nullabilitySuffix: NullabilitySuffix.none,
);
}
/// Returns whether the [parameter] is part of a constructor for a Flutter
/// `Widget`.
bool isFlutterWidgetParameter(FormalParameterElement parameter) {
var element = parameter.enclosingElement2;
if (element is ConstructorElement && element.enclosingElement2.isWidget) {
return true;
}
return false;
}
/// Return name of the type of the given [identifier], or, if it unresolved, the
/// name of its declared [declaredType].
String? nameForType(SimpleIdentifier identifier, TypeAnnotation? declaredType) {
// Get the type from the identifier element.
DartType type;
var element = identifier.element;
if (element == null) {
return DYNAMIC;
} else if (element is FunctionTypedElement) {
if (element is PropertyAccessorElement && element is SetterElement) {
return null;
}
type = element.returnType;
} else if (element is TypeAliasElement) {
var aliasedType = element.aliasedType;
if (aliasedType is FunctionType) {
type = aliasedType.returnType;
} else {
return null;
}
} else if (element is VariableElement) {
type = element.type;
} else {
return null;
}
// If the type is unresolved, use the declared type.
if (type is DynamicType) {
if (declaredType is NamedType) {
return declaredType.qualifiedName;
}
return DYNAMIC;
}
return type.getDisplayString();
}
class CompletionDefaultArgumentList {
final String? text;
final List<int>? ranges;
CompletionDefaultArgumentList({required this.text, required this.ranges});
}
/// A tuple of text to insert and an (optional) location for the cursor.
class DefaultArgument {
/// The text to insert.
final String text;
/// An optional location for the cursor, relative to the text's start. This
/// field can be null.
final int? cursorPosition;
DefaultArgument(this.text, {this.cursorPosition});
}