blob: f923c6f6dbf4fd44e6b3d5c413e79e6d9bd9ff6e [file] [log] [blame]
// Copyright (c) 2015, 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:_fe_analyzer_shared/src/base/syntactic_entity.dart';
import 'package:analysis_server/src/computer/computer_hover.dart';
import 'package:analysis_server/src/protocol_server.dart'
hide Element, ElementKind;
import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/dart_completion_suggestion.dart';
import 'package:analysis_server/src/services/completion/dart/relevance_computer.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analysis_server/src/utilities/extensions/element.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_range.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
/// A container with enough information to do filtering, and if necessary
/// build the [CompletionSuggestion] instance.
abstract class CompletionSuggestionBuilder {
/// See [CompletionSuggestion.completion].
String get completion;
/// The kind of the element, if there is the associated element.
/// We use it for completion metrics, to avoid [build].
protocol.ElementKind? get elementKind;
/// The key used to de-duplicate suggestions.
String get key => completion;
/// See [CompletionSuggestion.kind].
CompletionSuggestionKind get kind;
/// See [CompletionSuggestion.relevance].
int get relevance;
/// Return the text that should be matched against the filter.
String get textToMatch;
CompletionSuggestion build();
}
/// An object used to build a list of suggestions in response to a single
/// completion request.
class SuggestionBuilder {
/// The completion request for which suggestions are being built.
final DartCompletionRequest request;
/// The listener to be notified at certain points in the process of building
/// suggestions, or `null` if no notification should occur.
final SuggestionListener? listener;
/// A map from a completion identifier to a completion suggestion.
final Map<String, CompletionSuggestionBuilder> _suggestionMap = {};
/// The URI of the library from which suggestions are being added.
/// This URI is not necessary the same as the URI that declares an element,
/// because of exports.
String? libraryUriStr;
/// URIs that should be imported (that are not already) for all types in the
/// completion.
///
/// Includes a [URI] for [libraryUriStr] only if the items being suggested are
/// not already imported.
List<Uri> requiredImports = const [];
/// This flag is set to `true` while adding suggestions for top-level
/// elements from not-yet-imported libraries.
bool isNotImportedLibrary = false;
/// A flag indicating whether a suggestion should replace any earlier
/// suggestions for the same completion (`true`) or whether earlier
/// suggestions should take priority over more recent suggestions.
// TODO(brianwilkerson): Attempt to convert the contributors so that a single
// approach is followed.
bool laterReplacesEarlier = true;
/// A flag indicating whether the [_cachedContainingMemberName] has been
/// computed.
bool _hasContainingMemberName = false;
/// The name of the member containing the completion location, or `null` if
/// either the completion location isn't within a member, the target of the
/// completion isn't `super`, or the name of the member hasn't yet been
/// computed. In the latter case, [_hasContainingMemberName] will be `false`.
String? _cachedContainingMemberName;
/// If added builders can be filtered based on the `targetPrefix`.
final bool useFilter;
/// The lower case `targetPrefix` possibly used to filter results before
/// actually adding them.
late final String targetPrefixLower;
/// Used to compute the relevance of the completion suggestion.
final RelevanceComputer relevanceComputer;
/// Initialize a newly created suggestion builder to build suggestions for the
/// given [request].
SuggestionBuilder(this.request, {this.listener, required this.useFilter})
: relevanceComputer = RelevanceComputer(request, listener) {
targetPrefixLower = request.targetPrefix.toLowerCase();
}
/// Return an iterable that can be used to access the completion suggestions
/// that have been built.
Iterable<CompletionSuggestionBuilder> get suggestions =>
_suggestionMap.values;
/// Return the name of the member containing the completion location, or
/// `null` if the completion location isn't within a member or if the target
/// of the completion isn't `super`.
String? get _containingMemberName {
if (!_hasContainingMemberName) {
_hasContainingMemberName = true;
if (request.target.dotTarget is SuperExpression) {
var containingMethod = request.target.containingNode
.thisOrAncestorOfType<MethodDeclaration>();
if (containingMethod != null) {
_cachedContainingMemberName = containingMethod.name.lexeme;
}
}
}
return _cachedContainingMemberName;
}
/// Return `true` if the context requires a constant expression.
bool get _preferConstants =>
request.inConstantContext || request.opType.mustBeConst;
/// Add a suggestion for an [accessor] declared within a class or extension.
/// If the accessor is being invoked with a target of `super`, then the
/// [containingMemberName] should be the name of the member containing the
/// invocation. The [inheritanceDistance] is the value of the inheritance
/// distance feature computed for the accessor or `-1.0` if the accessor is a
/// static accessor.
void suggestAccessor(
PropertyAccessorElement accessor, {
required double inheritanceDistance,
bool withEnclosingName = false,
int? relevance,
String? completion,
}) {
var enclosingPrefix = '';
var enclosingName = _enclosingClassOrExtensionName(accessor);
if (withEnclosingName && enclosingName != null) {
enclosingPrefix = '$enclosingName.';
}
completion ??= enclosingPrefix + accessor.displayName;
if (_couldMatch(completion, null)) {
var type = _getPropertyAccessorType(accessor);
var featureComputer = request.featureComputer;
var contextType =
featureComputer.contextTypeFeature(request.contextType, type);
var elementKind =
_computeElementKind(accessor, distance: inheritanceDistance);
var hasDeprecated = featureComputer.hasDeprecatedFeature(accessor);
var isConstant =
_preferConstants ? featureComputer.isConstantFeature(accessor) : 0.0;
var startsWithDollar =
featureComputer.startsWithDollarFeature(accessor.name);
var superMatches = featureComputer.superMatchesFeature(
_containingMemberName, accessor.name);
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
hasDeprecated: hasDeprecated,
isConstant: isConstant,
isNotImported:
request.featureComputer.isNotImportedFeature(isNotImportedLibrary),
startsWithDollar: startsWithDollar,
superMatches: superMatches,
inheritanceDistance: inheritanceDistance,
);
_addBuilder(
_createCompletionSuggestionBuilder(
accessor,
completion: completion,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion to insert a closure.
void suggestClosure(
{required String completion,
required String displayText,
required int selectionOffset}) {
_addSuggestion(DartCompletionSuggestion(
CompletionSuggestionKind.INVOCATION,
Relevance.closure,
completion,
selectionOffset,
0,
false,
false,
displayText: displayText,
elementLocation: null, // type.element is Null for FunctionType.
));
}
/// Add a suggestion for a [constructor]. If a [kind] is provided it will be
/// used as the kind for the suggestion. The flag [hasClassName] should be
/// `true` if the completion is occurring after the name of the class and a
/// period, and hence should not include the name of the class. If the class
/// can only be referenced using a prefix, and the class name is to be
/// included in the completion, then the [prefix] should be provided.
void suggestConstructor(
ConstructorElement constructor, {
CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool suggestUnnamedAsNew = false,
bool hasClassName = false,
String? prefix,
int? relevance,
String? completion,
}) {
// If the class name is already in the text, then we don't support
// prepending a prefix.
assert(!hasClassName || prefix == null);
var enclosingClass = constructor.enclosingElement3.augmented.declaration;
if (completion == null) {
var className = enclosingClass.name;
if (className.isEmpty) {
return;
}
completion = constructor.name;
if (completion.isEmpty && suggestUnnamedAsNew) {
completion = 'new';
}
if (!hasClassName) {
if (completion.isEmpty) {
completion = className;
} else {
completion = '$className.$completion';
}
}
if (completion.isEmpty) {
return;
}
}
if (_couldMatch(completion, prefix)) {
var returnType = _instantiateInstanceElement(enclosingClass);
relevance ??= relevanceComputer.computeTopLevelRelevance(constructor,
elementType: returnType, isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
constructor,
completion: completion,
kind: kind,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for an enum [constant]. If the enum can only be
/// referenced using a prefix, then the [prefix] should be provided.
void suggestEnumConstant(FieldElement constant,
{String? prefix, int? relevance}) {
var constantName = constant.name;
var enumElement = constant.enclosingElement3;
var enumName = enumElement.name;
var completion = '$enumName.$constantName';
relevance ??= relevanceComputer.computeTopLevelRelevance(constant,
elementType: constant.type, isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
constant,
completion: completion,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion for an [extension]. If a [kind] is provided it will be
/// used as the kind for the suggestion. If the extension can only be
/// referenced using a prefix, then the [prefix] should be provided.
void suggestExtension(ExtensionElement extension,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
String? prefix,
int? relevance}) {
var completion = _getCompletionString(extension);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
relevance ??= relevanceComputer.computeTopLevelRelevance(extension,
elementType: extension.extendedType,
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
extension,
kind: kind,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for a [field]. If the field is being referenced with a
/// target of `super`, then the [containingMemberName] should be the name of
/// the member containing the reference. The [inheritanceDistance] is the
/// value of the inheritance distance feature computed for the field (or
/// `-1.0` if the field is a static field).
void suggestField(FieldElement field,
{required double inheritanceDistance, int? relevance}) {
var completion = _getCompletionString(field);
if (completion == null) return;
if (_couldMatch(completion, null)) {
var featureComputer = request.featureComputer;
var contextType =
featureComputer.contextTypeFeature(request.contextType, field.type);
var elementKind =
_computeElementKind(field, distance: inheritanceDistance);
var hasDeprecated = featureComputer.hasDeprecatedFeature(field);
var isConstant =
_preferConstants ? featureComputer.isConstantFeature(field) : 0.0;
var startsWithDollar =
featureComputer.startsWithDollarFeature(field.name);
var superMatches = featureComputer.superMatchesFeature(
_containingMemberName, field.name);
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
hasDeprecated: hasDeprecated,
isConstant: isConstant,
startsWithDollar: startsWithDollar,
superMatches: superMatches,
inheritanceDistance: inheritanceDistance,
);
_addBuilder(
_createCompletionSuggestionBuilder(
field,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
void suggestFormalParameter({
required ParameterElement element,
required int distance,
int? relevance,
}) {
var variableType = element.type;
var contextType = request.featureComputer
.contextTypeFeature(request.contextType, variableType);
var localVariableDistance =
request.featureComputer.distanceToPercent(distance);
var elementKind = _computeElementKind(element);
var isConstant = _preferConstants
? request.featureComputer.isConstantFeature(element)
: 0.0;
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
isConstant: isConstant,
localVariableDistance: localVariableDistance,
);
_addBuilder(
_createCompletionSuggestionBuilder(
element,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion for the `call` method defined on functions.
void suggestFunctionCall() {
var element = protocol.Element(protocol.ElementKind.METHOD,
FunctionElement.CALL_METHOD_NAME, protocol.Element.makeFlags(),
parameters: '()', returnType: 'void');
_addSuggestion(
CompletionSuggestion(
CompletionSuggestionKind.INVOCATION,
Relevance.callFunction,
FunctionElement.CALL_METHOD_NAME,
FunctionElement.CALL_METHOD_NAME.length,
0,
false,
false,
displayText: 'call()',
element: element,
returnType: 'void',
parameterNames: [],
parameterTypes: [],
requiredParameterCount: 0,
hasNamedParameters: false,
),
);
}
/// Add a suggestion for an [element]. If the class can only be
/// referenced using a prefix, then the [prefix] should be provided.
void suggestInterface(InterfaceElement element,
{String? prefix, int? relevance}) {
var completion = _getCompletionString(element);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
relevance ??= relevanceComputer.computeTopLevelRelevance(element,
elementType: _instantiateInstanceElement(element),
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
element,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for a [keyword]. The [offset] is the offset from the
/// beginning of the keyword where the cursor will be left.
void suggestKeyword(String keyword, {int? offset, int? relevance}) {
DartType? elementType;
if (keyword == 'null') {
elementType = request.featureComputer.typeProvider.nullType;
} else if (keyword == 'false' || keyword == 'true') {
elementType = request.featureComputer.typeProvider.boolType;
}
var contextType = request.featureComputer
.contextTypeFeature(request.contextType, elementType);
var keywordFeature = request.featureComputer
.keywordFeature(keyword, request.opType.completionLocation);
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
keyword: keywordFeature,
);
_addSuggestion(CompletionSuggestion(CompletionSuggestionKind.KEYWORD,
relevance, keyword, offset ?? keyword.length, 0, false, false));
}
/// Add a suggestion for a [label].
void suggestLabel(Label label) {
var completion = label.label.name;
// TODO(brianwilkerson): Figure out why we're excluding labels consisting of
// a single underscore.
if (completion.isNotEmpty && completion != '_') {
var suggestion = CompletionSuggestion(CompletionSuggestionKind.IDENTIFIER,
Relevance.label, completion, completion.length, 0, false, false);
suggestion.element = createLocalElement(
request.source, protocol.ElementKind.LABEL, label.label);
_addSuggestion(suggestion);
}
}
/// Add a suggestion for the `loadLibrary` [function] associated with a
/// prefix.
void suggestLoadLibraryFunction(FunctionElement function) {
// TODO(brianwilkerson): This might want to use the context type rather than
// a fixed value.
var relevance = Relevance.loadLibrary;
_addBuilder(
_createCompletionSuggestionBuilder(
function,
kind: CompletionSuggestionKind.INVOCATION,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
void suggestLocalVariable({
required LocalVariableElement element,
required int distance,
int? relevance,
}) {
var variableType = element.type;
var contextType = request.featureComputer
.contextTypeFeature(request.contextType, variableType);
var localVariableDistance =
request.featureComputer.distanceToPercent(distance);
var elementKind =
_computeElementKind(element, distance: localVariableDistance);
var isConstant = _preferConstants
? request.featureComputer.isConstantFeature(element)
: 0.0;
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
isConstant: isConstant,
localVariableDistance: localVariableDistance,
);
_addBuilder(
_createCompletionSuggestionBuilder(
element,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion for a [method]. If a [kind] is provided it will be used
/// as the kind for the suggestion. The [inheritanceDistance] is the value of
/// the inheritance distance feature computed for the method.
void suggestMethod(MethodElement method,
{required CompletionSuggestionKind kind,
required double inheritanceDistance,
int? relevance}) {
// TODO(brianwilkerson): Refactor callers so that we're passing in the type
// of the target (assuming we don't already have that type available via
// the [request]) and compute the [inheritanceDistance] in this method.
var featureComputer = request.featureComputer;
var contextType = featureComputer.contextTypeFeature(
request.contextType, method.returnType);
var elementKind =
_computeElementKind(method, distance: inheritanceDistance);
var hasDeprecated = featureComputer.hasDeprecatedFeature(method);
var isConstant =
_preferConstants ? featureComputer.isConstantFeature(method) : 0.0;
var isNoSuchMethod = featureComputer.isNoSuchMethodFeature(
_containingMemberName, method.name);
var startsWithDollar = featureComputer.startsWithDollarFeature(method.name);
var superMatches =
featureComputer.superMatchesFeature(_containingMemberName, method.name);
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
hasDeprecated: hasDeprecated,
isConstant: isConstant,
isNoSuchMethod: isNoSuchMethod,
isNotImported:
request.featureComputer.isNotImportedFeature(isNotImportedLibrary),
startsWithDollar: startsWithDollar,
superMatches: superMatches,
inheritanceDistance: inheritanceDistance,
);
_addBuilder(
_createCompletionSuggestionBuilder(
method,
kind: kind,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion to use the [name] at a declaration site.
void suggestName(String name, {int? selectionOffset}) {
// TODO(brianwilkerson): Explore whether there are any features of the name
// that can be used to provide better relevance scores.
_addSuggestion(CompletionSuggestion(CompletionSuggestionKind.IDENTIFIER,
500, name, selectionOffset ?? name.length, 0, false, false));
}
/// Add a suggestion to add a named argument corresponding to the [parameter].
/// If [appendColon] is `true` then a colon will be added after the name. If
/// [appendComma] is `true` then a comma will be included at the end of the
/// completion text.
void suggestNamedArgument(ParameterElement parameter,
{required bool appendColon,
required bool appendComma,
required int selectionOffset,
required String completion,
int? replacementLength,
required int relevance}) {
var name = parameter.name;
var type = parameter.type.getDisplayString();
var suggestion = DartCompletionSuggestion(
CompletionSuggestionKind.NAMED_ARGUMENT,
relevance,
completion,
selectionOffset,
0,
false,
false,
parameterName: name,
parameterType: type,
replacementLength: replacementLength,
element: convertElement(parameter),
elementLocation: parameter.location);
if (parameter is FieldFormalParameterElement) {
_setDocumentation(suggestion, parameter);
}
_addSuggestion(suggestion);
}
/// Add a suggestion to add a named argument corresponding to the [field].
/// If [appendColon] is `true` then a colon will be added after the name. If
/// [appendComma] is `true` then a comma will be included at the end of the
/// completion text.
void suggestNamedRecordField(RecordTypeNamedField field,
{required bool appendColon,
required bool appendComma,
int? replacementLength}) {
var name = field.name;
var type = field.type.getDisplayString();
var completion = name;
if (appendColon) {
completion += ': ';
}
var selectionOffset = completion.length;
if (appendComma) {
completion += ',';
}
_addSuggestion(
CompletionSuggestion(
CompletionSuggestionKind.NAMED_ARGUMENT,
Relevance.requiredNamedArgument,
completion,
selectionOffset,
0,
false,
false,
parameterName: name,
parameterType: type,
replacementLength: replacementLength,
),
);
}
/// Add a suggestion to replace the [targetId] with an override of the given
/// [element]. If [invokeSuper] is `true`, then the override will contain an
/// invocation of an overridden member.
Future<void> suggestOverride({
required ExecutableElement element,
required bool invokeSuper,
required SourceRange replacementRange,
required bool skipAt,
}) async {
var displayTextBuffer = StringBuffer();
var overrideImports = <Uri>{};
var builder = ChangeBuilder(session: request.analysisSession);
await builder.addDartFileEdit(request.path, createEditsForImports: false,
(builder) {
builder.addReplacement(replacementRange, (builder) {
builder.writeOverride(
element,
displayTextBuffer: displayTextBuffer,
invokeSuper: invokeSuper,
);
});
overrideImports.addAll(builder.requiredImports);
});
var fileEdits = builder.sourceChange.edits;
if (fileEdits.length != 1) {
return;
}
var sourceEdits = fileEdits[0].edits;
if (sourceEdits.length != 1) {
return;
}
var replacement = sourceEdits[0].replacement;
var completion = replacement.trim();
var overrideAnnotation = '@override';
if (request.target.containingNode.hasOverride &&
completion.startsWith(overrideAnnotation)) {
completion = completion.substring(overrideAnnotation.length).trim();
}
if (skipAt && completion.startsWith(overrideAnnotation)) {
completion = completion.substring('@'.length);
}
if (completion.isEmpty) {
return;
}
var selectionRange = builder.selectionRange;
if (selectionRange == null) {
return;
}
var offsetDelta = replacementRange.offset + replacement.indexOf(completion);
var displayText = displayTextBuffer.toString();
if (displayText.isEmpty) {
return;
}
if (skipAt) {
displayText = 'override $displayText';
}
var suggestion = DartCompletionSuggestion(
CompletionSuggestionKind.OVERRIDE,
Relevance.override,
completion,
selectionRange.offset - offsetDelta,
selectionRange.length,
element.hasDeprecated,
false,
displayText: displayText,
elementLocation: element.location,
requiredImports: overrideImports.toList());
suggestion.element = protocol.convertElement(element);
_addSuggestion(
suggestion,
textToMatchOverride: _textToMatchOverride(element),
);
}
/// Add a suggestion for a [prefix] associated with a [library].
void suggestPrefix(LibraryElement library, String prefix, {int? relevance}) {
var elementKind = _computeElementKind(library);
// TODO(brianwilkerson): If we are in a constant context it would be nice
// to promote prefixes for libraries that define constants, but that
// might be more work than it's worth.
relevance ??= relevanceComputer.computeScore(
elementKind: elementKind,
);
_addBuilder(
_createCompletionSuggestionBuilder(
library,
completion: prefix,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
void suggestRecordField({
required RecordTypeField field,
required String name,
int? relevance,
}) {
var type = field.type;
var featureComputer = request.featureComputer;
var contextType =
featureComputer.contextTypeFeature(request.contextType, type);
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
);
var returnType = field.type.getDisplayString();
_addSuggestion(
CompletionSuggestion(
CompletionSuggestionKind.IDENTIFIER,
relevance,
name,
name.length,
0,
false,
false,
returnType: returnType,
),
);
}
/// Add a suggestion for the Flutter's `setState` method.
void suggestSetStateMethod(MethodElement method,
{required CompletionSuggestionKind kind,
required String completion,
required String displayText,
required int selectionOffset,
required double inheritanceDistance,
required int relevance}) {
_addSuggestion(
DartCompletionSuggestion(
kind,
relevance,
completion,
selectionOffset,
0,
false,
false,
// Let the user know that we are going to insert a complete statement.
displayText: displayText,
elementLocation: method.location,
),
textToMatchOverride: 'setState',
);
}
/// Add a suggestion for a static field declared within a class or extension.
/// If the field is synthetic, add the corresponding getter instead.
///
/// If the enclosing element can only be referenced using a prefix, then
/// the [prefix] should be provided.
void suggestStaticField(FieldElement element,
{String? prefix, int? relevance}) {
assert(element.isStatic);
var enclosingPrefix = '';
var enclosingName = _enclosingClassOrExtensionName(element);
if (enclosingName != null) {
enclosingPrefix = '$enclosingName.';
}
var completion = enclosingPrefix + element.name;
if (_couldMatch(completion, prefix)) {
relevance ??= relevanceComputer.computeTopLevelRelevance(element,
elementType: element.type,
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
element,
completion: completion,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion to reference a [parameter] in a super formal parameter.
void suggestSuperFormalParameter(ParameterElement parameter) {
_addBuilder(
_createCompletionSuggestionBuilder(
parameter,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: Relevance.superFormalParameter,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion for a top-level [function]. If a [kind] is provided it
/// will be used as the kind for the suggestion. If the function can only be
/// referenced using a prefix, then the [prefix] should be provided.
void suggestTopLevelFunction(FunctionElement function,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
String? prefix,
int? relevance}) {
var completion = _getCompletionString(function);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
relevance ??= relevanceComputer.computeTopLevelRelevance(function,
elementType: function.returnType,
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
function,
kind: kind,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for a top-level property [accessor]. If the accessor can
/// only be referenced using a prefix, then the [prefix] should be provided.
void suggestTopLevelPropertyAccessor(PropertyAccessorElement accessor,
{String? prefix, int? relevance}) {
assert(
accessor.enclosingElement3 is CompilationUnitElement,
'Enclosing element of ${accessor.runtimeType} is '
'${accessor.enclosingElement3.runtimeType}.');
if (accessor.isSynthetic) {
// Avoid visiting a field twice. All fields induce a getter, but only
// non-final fields induce a setter, so we don't add a suggestion for a
// synthetic setter.
if (accessor.isGetter) {
var variable = accessor.variable2;
if (variable is TopLevelVariableElement) {
suggestTopLevelVariable(variable);
}
}
} else {
var completion = _getCompletionString(accessor);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
var type = _getPropertyAccessorType(accessor);
var featureComputer = request.featureComputer;
var contextType =
featureComputer.contextTypeFeature(request.contextType, type);
var elementKind = _computeElementKind(accessor);
var hasDeprecated = featureComputer.hasDeprecatedFeature(accessor);
var isConstant = _preferConstants
? featureComputer.isConstantFeature(accessor)
: 0.0;
var startsWithDollar =
featureComputer.startsWithDollarFeature(accessor.name);
var superMatches = 0.0;
relevance ??= relevanceComputer.computeScore(
contextType: contextType,
elementKind: elementKind,
hasDeprecated: hasDeprecated,
isConstant: isConstant,
isNotImported: request.featureComputer
.isNotImportedFeature(isNotImportedLibrary),
startsWithDollar: startsWithDollar,
superMatches: superMatches,
);
_addBuilder(
_createCompletionSuggestionBuilder(
accessor,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
}
/// Add a suggestion for a top-level [variable]. If the variable can only be
/// referenced using a prefix, then the [prefix] should be provided.
void suggestTopLevelVariable(TopLevelVariableElement variable,
{String? prefix, int? relevance}) {
var completion = _getCompletionString(variable);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
assert(variable.enclosingElement3 is CompilationUnitElement);
relevance ??= relevanceComputer.computeTopLevelRelevance(variable,
elementType: variable.type,
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
variable,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for a [typeAlias]. If the alias can only be referenced
/// using a prefix, then the [prefix] should be provided.
void suggestTypeAlias(TypeAliasElement typeAlias,
{String? prefix, int? relevance}) {
var completion = _getCompletionString(typeAlias);
if (completion == null) return;
if (_couldMatch(completion, prefix)) {
relevance ??= relevanceComputer.computeTopLevelRelevance(typeAlias,
elementType: _instantiateTypeAlias(typeAlias),
isNotImportedLibrary: isNotImportedLibrary);
_addBuilder(
_createCompletionSuggestionBuilder(
typeAlias,
kind: CompletionSuggestionKind.IDENTIFIER,
prefix: prefix,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
}
/// Add a suggestion for a type [parameter].
void suggestTypeParameter(TypeParameterElement parameter, {int? relevance}) {
var elementKind = _computeElementKind(parameter);
var isConstant = _preferConstants
? request.featureComputer.isConstantFeature(parameter)
: 0.0;
relevance ??= relevanceComputer.computeScore(
elementKind: elementKind,
isConstant: isConstant,
);
_addBuilder(
_createCompletionSuggestionBuilder(
parameter,
kind: CompletionSuggestionKind.IDENTIFIER,
relevance: relevance,
isNotImported: isNotImportedLibrary,
),
);
}
/// Add a suggestion to use the [uri] in an import, export, or part directive.
void suggestUri(String uri) {
var relevance =
uri == 'dart:core' ? Relevance.importDartCore : Relevance.import;
_addSuggestion(
CompletionSuggestion(CompletionSuggestionKind.IMPORT, relevance, uri,
uri.length, 0, false, false),
);
}
/// Add the given [suggestion] if it isn't `null` and if it isn't shadowed by
/// a previously added suggestion.
void _addBuilder(CompletionSuggestionBuilder? suggestion) {
if (suggestion != null) {
var key = suggestion.key;
listener?.builtSuggestion(suggestion);
if (laterReplacesEarlier || !_suggestionMap.containsKey(key)) {
// TODO(brianwilkerson): Add some specific tests of shadowing behavior.
if (suggestion is _CompletionSuggestionBuilderImpl) {
// We need to special-case constructors because the order in which
// suggestions are added has been changed by the move to
// `InScopeCompletionPass`.
var suggestedElement = suggestion.orgElement;
if (suggestedElement is ConstructorElement) {
var parentName = suggestedElement.enclosingElement3.name;
var existingSuggestion = _suggestionMap[parentName];
if (existingSuggestion is _CompletionSuggestionBuilderImpl &&
existingSuggestion.orgElement is! ClassElement) {
// We return when the current suggestion is not a class because that
// means that the current suggestion shadows the one being added.
return;
}
}
}
// When suggesting from not-yet-imported libraries, record items
// with a key that includes the URI so that multiple not-yet-imported
// libraries can be included, but only if there is no imported library
// contributing that key.
if (isNotImportedLibrary) {
key += '::$libraryUriStr';
// If `!laterReplacesEarlier`, also ensure we don't already have this
// new key.
if (!laterReplacesEarlier && _suggestionMap.containsKey(key)) {
return;
}
}
_suggestionMap[key] = suggestion;
}
}
}
/// Add the given [suggestion] if it isn't shadowed by a previously added
/// suggestion.
void _addSuggestion(
protocol.CompletionSuggestion suggestion, {
String? textToMatchOverride,
}) {
_addBuilder(
ValueCompletionSuggestionBuilder(
suggestion,
textToMatchOverride: textToMatchOverride,
),
);
}
/// Compute the value of the _element kind_ feature for the given [element] in
/// the completion context.
double _computeElementKind(Element element, {double? distance}) {
var location = request.opType.completionLocation;
var elementKind = request.featureComputer
.elementKindFeature(element, location, distance: distance);
if (elementKind < 0.0) {
if (location == null) {
listener?.missingCompletionLocationAt(
request.target.containingNode, request.target.entity!);
} else {
listener?.missingElementKindTableFor(location);
}
}
return elementKind;
}
bool _couldMatch(String candidateArbitraryCase, String? prefix) {
if (!useFilter) return true;
var candidateLower = candidateArbitraryCase.toLowerCase();
if (prefix != null) {
candidateLower = '${prefix.toLowerCase()}.$candidateLower';
}
var i = 0;
var j = 0;
for (; i < candidateLower.length && j < targetPrefixLower.length; i++) {
if (candidateLower.codeUnitAt(i) == targetPrefixLower.codeUnitAt(j)) {
j++;
}
}
return j == targetPrefixLower.length;
}
/// Return a [CompletionSuggestionBuilder] based on the [element], or `null`
/// if the element cannot be suggested. If the completion should be something
/// different than the name of the element, then the [completion] should be
/// supplied. If an [elementKind] is provided, then it will be used rather
/// than the kind normally used for the element. If a [prefix] is provided,
/// then the element name (or completion) will be prefixed. The [relevance] is
/// the relevance of the suggestion.
CompletionSuggestionBuilder? _createCompletionSuggestionBuilder(
Element element, {
String? completion,
required CompletionSuggestionKind kind,
required int relevance,
required bool isNotImported,
String? prefix,
}) {
completion ??= _getCompletionString(element);
if (completion == null) {
return null;
}
if (prefix != null) {
completion = '$prefix.$completion';
}
return _CompletionSuggestionBuilderImpl(
orgElement: element,
suggestionBuilder: this,
kind: kind,
completion: completion,
relevance: relevance,
libraryUriStr: libraryUriStr,
requiredImports: requiredImports,
isNotImported: isNotImported,
);
}
/// The non-caching implementation of [_getElementCompletionData].
_ElementCompletionData _createElementCompletionData(Element element) {
var documentation = _getDocumentation(element);
var suggestedElement = protocol.convertElement(element);
var enclosingElement = element.enclosingElement3;
String? declaringType;
if (enclosingElement is InterfaceElement) {
declaringType = enclosingElement.displayName;
}
var returnType = getReturnTypeString(element);
var colorHex = getColorHexString(element);
List<String>? parameterNames;
List<String>? parameterTypes;
int? requiredParameterCount;
bool? hasNamedParameters;
CompletionDefaultArgumentList? defaultArgumentList;
if (element is ExecutableElement && element is! PropertyAccessorElement) {
parameterNames = element.parameters.map((parameter) {
return parameter.name;
}).toList();
parameterTypes = element.parameters.map((ParameterElement parameter) {
return parameter.type.getDisplayString();
}).toList();
var requiredParameters = element.parameters
.where((ParameterElement param) => param.isRequiredPositional);
requiredParameterCount = requiredParameters.length;
var namedParameters =
element.parameters.where((ParameterElement param) => param.isNamed);
hasNamedParameters = namedParameters.isNotEmpty;
defaultArgumentList = computeCompletionDefaultArgumentList(
element, requiredParameters, namedParameters);
}
return _ElementCompletionData(
isDeprecated: element.hasOrInheritsDeprecated,
declaringType: declaringType,
returnType: returnType,
parameterNames: parameterNames,
parameterTypes: parameterTypes,
requiredParameterCount: requiredParameterCount,
hasNamedParameters: hasNamedParameters,
documentation: documentation,
defaultArgumentList: defaultArgumentList,
element: suggestedElement,
elementLocation: element.location,
colorHex: colorHex,
);
}
/// Return the name of the enclosing class or extension.
///
/// The enclosing element must be either a class, or extension; otherwise
/// we either fail with assertion, or return `null`.
String? _enclosingClassOrExtensionName(Element element) {
var enclosing = element.enclosingElement3;
if (enclosing is InterfaceElement) {
return enclosing.name;
} else if (enclosing is ExtensionElement) {
return enclosing.name;
} else {
assert(false, 'Expected ClassElement or ExtensionElement');
return null;
}
}
String? _getCompletionString(Element element) {
if (element is ExecutableElement && element.isOperator) {
return null;
}
return element.displayName;
}
/// If the [element] has a documentation comment, return it.
_ElementDocumentation? _getDocumentation(Element element) {
var doc = DartUnitHoverComputer.computeDocumentation(
request.dartdocDirectiveInfo,
element,
includeSummary: true,
);
if (doc is DocumentationWithSummary) {
return _ElementDocumentation(
full: doc.full,
summary: doc.summary,
);
}
if (doc is Documentation) {
return _ElementDocumentation(
full: doc.full,
summary: null,
);
}
return null;
}
/// Return the type associated with the [accessor], maybe `null` if an
/// invalid setter with no parameters at all.
DartType? _getPropertyAccessorType(PropertyAccessorElement accessor) {
if (accessor.isGetter) {
return accessor.returnType;
} else {
var parameters = accessor.parameters;
if (parameters.isEmpty) {
return null;
} else {
return parameters[0].type;
}
}
}
InterfaceType _instantiateInstanceElement(InterfaceElement element) {
var typeParameters = element.typeParameters;
var typeArguments = const <DartType>[];
if (typeParameters.isNotEmpty) {
var neverType = request.libraryElement.typeProvider.neverType;
typeArguments = List.filled(typeParameters.length, neverType);
}
return element.instantiate(
typeArguments: typeArguments,
nullabilitySuffix: NullabilitySuffix.none,
);
}
DartType _instantiateTypeAlias(TypeAliasElement element) {
var typeParameters = element.typeParameters;
var typeArguments = const <DartType>[];
if (typeParameters.isNotEmpty) {
var neverType = request.libraryElement.typeProvider.neverType;
typeArguments = List.filled(typeParameters.length, neverType);
}
return element.instantiate(
typeArguments: typeArguments,
nullabilitySuffix: NullabilitySuffix.none,
);
}
/// If the [element] has a documentation comment, fill the [suggestion]'s
/// documentation fields.
void _setDocumentation(CompletionSuggestion suggestion, Element element) {
var doc = DartUnitHoverComputer.computeDocumentation(
request.dartdocDirectiveInfo, element,
includeSummary: true);
if (doc is DocumentationWithSummary) {
suggestion.docComplete = doc.full;
suggestion.docSummary = doc.summary;
}
}
static String _textToMatchOverride(ExecutableElement element) {
if (element.isOperator) {
return 'override_operator';
}
// Add "override" to match filter when `@override`.
return 'override_${element.displayName}';
}
}
abstract class SuggestionListener {
/// Invoked when a suggestion has been built.
void builtSuggestion(CompletionSuggestionBuilder suggestionBuilder);
/// Invoked with the values of the features that were computed in the process
/// of building a suggestion. This method is invoked prior to invoking
/// [builtSuggestion].
void computedFeatures(
{double contextType,
double elementKind,
double hasDeprecated,
double isConstant,
double isNoSuchMethod,
double isNotImported,
double keyword,
double startsWithDollar,
double superMatches,
// Dependent features
double inheritanceDistance,
double localVariableDistance});
/// Invoked when an element kind feature cannot be produced because there is
/// no completion location label associated with the completion offset.
void missingCompletionLocationAt(
AstNode containingNode, SyntacticEntity entity);
/// Invoked when an element kind feature cannot be produced because there is
/// no `elementKindRelevance` table associated with the [completionLocation].
void missingElementKindTableFor(String completionLocation);
}
/// [CompletionSuggestionBuilder] that is based on a [CompletionSuggestion].
class ValueCompletionSuggestionBuilder implements CompletionSuggestionBuilder {
final CompletionSuggestion _suggestion;
final String? _textToMatchOverride;
ValueCompletionSuggestionBuilder(
this._suggestion, {
String? textToMatchOverride,
}) : _textToMatchOverride = textToMatchOverride;
@override
String get completion => _suggestion.completion;
@override
protocol.ElementKind? get elementKind => _suggestion.element?.kind;
@override
String get key => completion;
@override
protocol.CompletionSuggestionKind get kind => _suggestion.kind;
@override
int get relevance => _suggestion.relevance;
@override
String get textToMatch => _textToMatchOverride ?? completion;
@override
CompletionSuggestion build() {
return _suggestion;
}
}
/// The implementation of [CompletionSuggestionBuilder] that is based on
/// [ElementCompletionData] and location specific information.
class _CompletionSuggestionBuilderImpl implements CompletionSuggestionBuilder {
final Element orgElement;
final SuggestionBuilder suggestionBuilder;
@override
final CompletionSuggestionKind kind;
@override
final int relevance;
@override
final String completion;
final String? libraryUriStr;
final List<Uri> requiredImports;
final bool isNotImported;
_CompletionSuggestionBuilderImpl({
required this.orgElement,
required this.suggestionBuilder,
required this.kind,
required this.completion,
required this.relevance,
required this.libraryUriStr,
required this.requiredImports,
required this.isNotImported,
});
@override
protocol.ElementKind? get elementKind => convertElementKind(orgElement.kind);
// TODO(scheglov): implement better key for not-yet-imported
@override
String get key {
var key = completion;
if (orgElement.kind == ElementKind.CONSTRUCTOR) {
key = '$key()';
}
return key;
}
@override
String get textToMatch => completion;
@override
CompletionSuggestion build() {
var element = suggestionBuilder._createElementCompletionData(orgElement);
return DartCompletionSuggestion(
kind,
relevance,
completion,
completion.length /*selectionOffset*/,
0 /*selectionLength*/,
element.isDeprecated,
false /*isPotential*/,
element: element.element,
docSummary: element.documentation?.summary,
docComplete: element.documentation?.full,
declaringType: element.declaringType,
returnType: element.returnType,
requiredParameterCount: element.requiredParameterCount,
hasNamedParameters: element.hasNamedParameters,
parameterNames: element.parameterNames,
parameterTypes: element.parameterTypes,
defaultArgumentListString: element.defaultArgumentList?.text,
defaultArgumentListTextRanges: element.defaultArgumentList?.ranges,
libraryUri: libraryUriStr,
isNotImported: isNotImported ? true : null,
elementLocation: element.elementLocation,
requiredImports: requiredImports,
colorHex: element.colorHex,
);
}
}
/// Information about an [Element] that does not depend on the location where
/// this element is suggested. For some often used elements, such as classes,
/// it might be cached, so created only once.
class _ElementCompletionData {
final bool isDeprecated;
final String? declaringType;
final String? returnType;
final List<String>? parameterNames;
final List<String>? parameterTypes;
final int? requiredParameterCount;
final bool? hasNamedParameters;
CompletionDefaultArgumentList? defaultArgumentList;
final _ElementDocumentation? documentation;
final protocol.Element element;
final ElementLocation? elementLocation;
final String? colorHex;
_ElementCompletionData({
required this.isDeprecated,
required this.declaringType,
required this.returnType,
required this.parameterNames,
required this.parameterTypes,
required this.requiredParameterCount,
required this.hasNamedParameters,
required this.defaultArgumentList,
required this.documentation,
required this.element,
required this.elementLocation,
required this.colorHex,
});
}
class _ElementDocumentation {
final String full;
final String? summary;
_ElementDocumentation({
required this.full,
required this.summary,
});
}