blob: 78fef2f2e8c75e462d3bf174954047c1fe7fc61c [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:analysis_server/src/protocol_server.dart';
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/arglist_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/combinator_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
import 'package:analysis_server/src/services/completion/dart/extension_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/services/completion/dart/field_formal_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/imported_reference_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/keyword_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/label_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/library_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/library_prefix_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/local_library_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/local_reference_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/named_constructor_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/not_imported_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/override_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/redirecting_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/relevance_tables.g.dart';
import 'package:analysis_server/src/services/completion/dart/static_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/services/completion/dart/type_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/uri_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/variable_name_contributor.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.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';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart';
import 'package:analyzer_plugin/src/utilities/completion/optype.dart';
/// Class that tracks how much time budget we have left.
class CompletionBudget {
static const Duration defaultDuration = Duration(milliseconds: 100);
final Duration _budget;
final Stopwatch _timer = Stopwatch()..start();
CompletionBudget(this._budget);
bool get isEmpty {
return _timer.elapsed > _budget;
}
Duration get left {
var result = _budget - _timer.elapsed;
return result.isNegative ? Duration.zero : result;
}
}
/// [DartCompletionManager] determines if a completion request is Dart specific
/// and forwards those requests to all [DartCompletionContributor]s.
class DartCompletionManager {
/// Time budget to computing suggestions.
final CompletionBudget budget;
/// If not `null`, then instead of using [ImportedReferenceContributor],
/// fill this set with kinds of elements that are applicable at the
/// completion location, so should be suggested from available suggestion
/// sets.
final Set<protocol.ElementKind>? includedElementKinds;
/// If [includedElementKinds] is not null, must be also not `null`, and
/// will be filled with names of all top-level declarations from all
/// included suggestion sets.
final Set<String>? includedElementNames;
/// If [includedElementKinds] is not null, must be also not `null`, and
/// will be filled with tags for suggestions that should be given higher
/// relevance than other included suggestions.
final List<IncludedSuggestionRelevanceTag>? includedSuggestionRelevanceTags;
/// The listener to be notified at certain points in the process of building
/// suggestions, or `null` if no notification should occur.
final SuggestionListener? listener;
/// If specified, will be filled with URIs of libraries that are not yet
/// imported, but could be imported into the requested target. Corresponding
/// [CompletionSuggestion] will have the import index into this list.
final List<Uri>? librariesToImport;
/// Initialize a newly created completion manager. The parameters
/// [includedElementKinds], [includedElementNames], and
/// [includedSuggestionRelevanceTags] must either all be `null` or must all be
/// non-`null`.
DartCompletionManager({
required this.budget,
this.includedElementKinds,
this.includedElementNames,
this.includedSuggestionRelevanceTags,
this.listener,
this.librariesToImport,
}) : assert((includedElementKinds != null &&
includedElementNames != null &&
includedSuggestionRelevanceTags != null) ||
(includedElementKinds == null &&
includedElementNames == null &&
includedSuggestionRelevanceTags == null));
Future<List<CompletionSuggestion>> computeSuggestions(
DartCompletionRequest request,
OperationPerformanceImpl performance, {
bool enableOverrideContributor = true,
bool enableUriContributor = true,
}) async {
request.checkAborted();
var pathContext = request.resourceProvider.pathContext;
if (!file_paths.isDart(pathContext, request.result.path)) {
return const <CompletionSuggestion>[];
}
// Don't suggest in comments.
if (request.target.isCommentText) {
return const <CompletionSuggestion>[];
}
request.checkAborted();
// Request Dart specific completions from each contributor
var builder = SuggestionBuilder(request, listener: listener);
var contributors = <DartCompletionContributor>[
ArgListContributor(request, builder),
CombinatorContributor(request, builder),
ExtensionMemberContributor(request, builder),
FieldFormalContributor(request, builder),
KeywordContributor(request, builder),
LabelContributor(request, builder),
LibraryMemberContributor(request, builder),
LibraryPrefixContributor(request, builder),
LocalLibraryContributor(request, builder),
LocalReferenceContributor(request, builder),
NamedConstructorContributor(request, builder),
if (enableOverrideContributor) OverrideContributor(request, builder),
RedirectingContributor(request, builder),
StaticMemberContributor(request, builder),
TypeMemberContributor(request, builder),
if (enableUriContributor) UriContributor(request, builder),
VariableNameContributor(request, builder),
];
if (includedElementKinds != null) {
_addIncludedElementKinds(request);
_addIncludedSuggestionRelevanceTags(request);
} else {
contributors.add(
ImportedReferenceContributor(request, builder),
);
}
final librariesToImport = this.librariesToImport;
if (librariesToImport != null) {
contributors.add(
NotImportedContributor(request, builder, budget, librariesToImport),
);
}
try {
for (var contributor in contributors) {
await performance.runAsync(
'DartCompletionManager - ${contributor.runtimeType}',
(_) async {
await contributor.computeSuggestions();
},
);
request.checkAborted();
}
} on InconsistentAnalysisException {
// The state of the code being analyzed has changed, so results are likely
// to be inconsistent. Just abort the operation.
throw AbortCompletion();
}
return builder.suggestions.toList();
}
void _addIncludedElementKinds(DartCompletionRequest request) {
var opType = request.opType;
if (!opType.includeIdentifiers) return;
var kinds = includedElementKinds;
if (kinds != null) {
if (opType.includeConstructorSuggestions) {
kinds.add(protocol.ElementKind.CONSTRUCTOR);
}
if (opType.includeTypeNameSuggestions) {
kinds.add(protocol.ElementKind.CLASS);
kinds.add(protocol.ElementKind.CLASS_TYPE_ALIAS);
kinds.add(protocol.ElementKind.ENUM);
kinds.add(protocol.ElementKind.FUNCTION_TYPE_ALIAS);
kinds.add(protocol.ElementKind.MIXIN);
kinds.add(protocol.ElementKind.TYPE_ALIAS);
}
if (opType.includeReturnValueSuggestions) {
kinds.add(protocol.ElementKind.CONSTRUCTOR);
kinds.add(protocol.ElementKind.ENUM_CONSTANT);
kinds.add(protocol.ElementKind.EXTENSION);
// Static fields.
kinds.add(protocol.ElementKind.FIELD);
kinds.add(protocol.ElementKind.FUNCTION);
// Static and top-level properties.
kinds.add(protocol.ElementKind.GETTER);
kinds.add(protocol.ElementKind.SETTER);
kinds.add(protocol.ElementKind.TOP_LEVEL_VARIABLE);
}
}
}
void _addIncludedSuggestionRelevanceTags(DartCompletionRequest request) {
final includedSuggestionRelevanceTags =
this.includedSuggestionRelevanceTags!;
var location = request.opType.completionLocation;
if (location != null) {
var locationTable = elementKindRelevance[location];
if (locationTable != null) {
var inConstantContext = request.inConstantContext;
for (var entry in locationTable.entries) {
var kind = entry.key.toString();
var elementBoost = (entry.value.upper * 100).floor();
includedSuggestionRelevanceTags
.add(IncludedSuggestionRelevanceTag(kind, elementBoost));
if (inConstantContext) {
includedSuggestionRelevanceTags.add(IncludedSuggestionRelevanceTag(
'$kind+const', elementBoost + 100));
}
}
}
}
var type = request.contextType;
if (type is InterfaceType) {
var element = type.element;
var tag = '${element.librarySource.uri}::${element.name}';
if (element.isEnum) {
includedSuggestionRelevanceTags.add(
IncludedSuggestionRelevanceTag(
tag,
RelevanceBoost.availableEnumConstant,
),
);
} else {
// TODO(brianwilkerson) This was previously used to boost exact type
// matches. For example, if the context type was `Foo`, then the class
// `Foo` and it's constructors would be given this boost. Now this
// boost will almost always be ignored because the element boost will
// be bigger. Find a way to use this boost without negating the element
// boost, which is how we get constructors to come before classes.
includedSuggestionRelevanceTags.add(
IncludedSuggestionRelevanceTag(
tag,
RelevanceBoost.availableDeclaration,
),
);
}
}
}
}
/// The information about a requested list of completions within a Dart file.
class DartCompletionRequest {
final CompletionPreference completionPreference;
/// Return the type imposed on the target's `containingNode` based on its
/// context, or `null` if the context does not impose any type.
final DartType? contextType;
/// Return the object used to resolve macros in Dartdoc comments.
final DartdocDirectiveInfo dartdocDirectiveInfo;
final DocumentationCache? documentationCache;
/// Return the object used to compute the values of the features used to
/// compute relevance scores for suggestions.
final FeatureComputer featureComputer;
/// Return the offset within the source at which the completion is being
/// requested.
final int offset;
/// The [OpType] which describes which types of suggestions would fit the
/// request.
final OpType opType;
/// The source range that represents the region of text that should be
/// replaced when a suggestion is selected.
final SourceRange replacementRange;
/// The analysis result for the file in which the completion is being
/// requested.
final ResolvedUnitResult result;
/// Return the source in which the completion is being requested.
final Source source;
/// Return the completion target. This determines what part of the parse tree
/// will receive the newly inserted text.
/// At a minimum, all declarations in the completion scope in [target.unit]
/// will be resolved if they can be resolved.
final CompletionTarget target;
bool _aborted = false;
factory DartCompletionRequest({
required ResolvedUnitResult resolvedUnit,
required int offset,
DartdocDirectiveInfo? dartdocDirectiveInfo,
CompletionPreference completionPreference = CompletionPreference.insert,
DocumentationCache? documentationCache,
}) {
var target = CompletionTarget.forOffset(resolvedUnit.unit, offset);
var featureComputer = FeatureComputer(
resolvedUnit.typeSystem,
resolvedUnit.typeProvider,
);
var contextType = featureComputer.computeContextType(
target.containingNode,
offset,
);
var opType = OpType.forCompletion(target, offset);
if (contextType != null && contextType.isVoid) {
opType.includeVoidReturnSuggestions = true;
}
return DartCompletionRequest._(
completionPreference: completionPreference,
contextType: contextType,
dartdocDirectiveInfo: dartdocDirectiveInfo ?? DartdocDirectiveInfo(),
documentationCache: documentationCache,
featureComputer: featureComputer,
offset: offset,
opType: opType,
replacementRange: target.computeReplacementRange(offset),
result: resolvedUnit,
source: resolvedUnit.unit.declaredElement!.source,
target: target,
);
}
DartCompletionRequest._({
required this.completionPreference,
required this.contextType,
required this.dartdocDirectiveInfo,
required this.documentationCache,
required this.featureComputer,
required this.offset,
required this.opType,
required this.replacementRange,
required this.result,
required this.source,
required this.target,
});
/// Return the feature set that was used to analyze the compilation unit in
/// which suggestions are being made.
FeatureSet get featureSet => libraryElement.featureSet;
/// Return `true` if free standing identifiers should be suggested
bool get includeIdentifiers {
return opType.includeIdentifiers;
}
/// Return `true` if the completion is occurring in a constant context.
bool get inConstantContext {
var entity = target.entity;
return entity is Expression && entity.inConstantContext;
}
/// Return the library element which contains the unit in which the completion
/// is occurring.
LibraryElement get libraryElement => result.libraryElement;
/// Answer the [DartType] for Object in dart:core
DartType get objectType => libraryElement.typeProvider.objectType;
/// The length of the text to be replaced if the remainder of the identifier
/// containing the cursor is to be replaced when the suggestion is applied
/// (that is, the number of characters in the existing identifier).
/// This will be different than the [replacementOffset] - [offset]
/// if the [offset] is in the middle of an existing identifier.
int get replacementLength => replacementRange.length;
/// The offset of the start of the text to be replaced.
/// This will be different than the [offset] used to request the completion
/// suggestions if there was a portion of an identifier before the original
/// [offset]. In particular, the [replacementOffset] will be the offset of the
/// beginning of said identifier.
int get replacementOffset => replacementRange.offset;
/// Return the resource provider associated with this request.
ResourceProvider get resourceProvider => result.session.resourceProvider;
/// Return the content of the [source] in which the completion is being
/// requested, or `null` if the content could not be accessed.
String? get sourceContents => result.content;
/// Return the [SourceFactory] of the request.
SourceFactory get sourceFactory {
var context = result.session.analysisContext as DriverBasedAnalysisContext;
return context.driver.sourceFactory;
}
/// Return prefix that already exists in the document for [target] or empty
/// string if unavailable. This can be used to filter the completion list to
/// items that already match the text to the left of the caret.
String get targetPrefix {
var entity = target.entity;
if (entity is Token) {
var prev = entity.previous;
if (prev != null && prev.end == offset && prev.isKeywordOrIdentifier) {
return prev.lexeme;
}
}
while (entity is AstNode) {
if (entity is SimpleIdentifier) {
var identifier = entity.name;
if (offset >= entity.offset && offset < entity.end) {
return identifier.substring(0, offset - entity.offset);
} else if (offset == entity.end) {
return identifier;
}
}
var children = entity.childEntities;
entity = children.isEmpty ? null : children.first;
}
return '';
}
/// Abort the current completion request.
void abort() {
_aborted = true;
}
/// Throw [AbortCompletion] if the completion request has been aborted.
void checkAborted() {
if (_aborted) {
throw AbortCompletion();
}
}
}