| // Copyright (c) 2025, 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/lsp/completion_utils.dart' |
| show createTypedSuggestionData; |
| import 'package:analysis_server/src/protocol_server.dart'; |
| import 'package:analysis_server/src/services/completion/dart/candidate_suggestion.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/utilities.dart'; |
| import 'package:analysis_server/src/utilities/extensions/ast.dart'; |
| import 'package:analysis_server/src/utilities/extensions/element.dart'; |
| import 'package:analyzer/dart/element/element.dart' as e; |
| import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/completion/relevance.dart'; |
| |
| /// Converts a [CandidateSuggestion] into a [CompletionSuggestion] object. |
| Future<CompletionSuggestion?> candidateToCompletionSuggestion( |
| CandidateSuggestion candidate, |
| DartCompletionRequest request, |
| ) async { |
| bool? isNotImportedLibrary; |
| var requiredImports = <Uri>[]; |
| String? libraryUriStr; |
| |
| if (candidate is ImportableSuggestion) { |
| var importData = candidate.importData; |
| if (importData != null) { |
| var uri = importData.libraryUri; |
| if (importData.isNotImported) { |
| isNotImportedLibrary = true; |
| requiredImports = [uri]; |
| } |
| libraryUriStr = uri.toString(); |
| } |
| } |
| |
| switch (candidate) { |
| case TypedSuggestion(): |
| var data = await createTypedSuggestionData(candidate, request); |
| requiredImports = data?.imports.toList() ?? requiredImports; |
| var kind = |
| request.target.isFunctionalArgument() |
| ? CompletionSuggestionKind.IDENTIFIER |
| : null; |
| candidate.data = data; |
| |
| return switch (candidate) { |
| FieldSuggestion() => _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| displayString: data?.displayText, |
| ), |
| |
| GetterSuggestion() => _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| displayString: data?.displayText, |
| ), |
| |
| SetStateMethodSuggestion() => DartCompletionSuggestion( |
| candidate.kind, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| displayText: data?.displayText ?? candidate.displayText, |
| ), |
| |
| MethodSuggestion(kind: var suggestionKind) => |
| // TODO(brianwilkerson): Correctly set the kind of suggestion in cases |
| // where `isFunctionalArgument` would return `true` so we can stop |
| // using the `request.target`. |
| _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| kind ?? suggestionKind, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| displayString: data?.displayText, |
| ), |
| RecordFieldSuggestion() => DartCompletionSuggestion( |
| CompletionSuggestionKind.IDENTIFIER, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.completion.length, |
| 0, |
| false, |
| false, |
| returnType: candidate.field.type.getDisplayString(), |
| displayText: data?.displayText ?? candidate.name, |
| ), |
| |
| // This is a workaround because mixins can't be `sealed`. |
| TypedSuggestionCompletionMixin() => null, |
| }; |
| |
| case ClassSuggestion(): |
| return _getInterfaceSuggestion( |
| candidate, |
| candidate.element, |
| request, |
| libraryUriStr, |
| isNotImportedLibrary, |
| requiredImports, |
| ); |
| case ClosureSuggestion(): |
| return DartCompletionSuggestion( |
| CompletionSuggestionKind.INVOCATION, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| displayText: candidate.displayText, |
| ); |
| case ConstructorSuggestion(): |
| var completion = candidate.completion; |
| if (completion.isEmpty) { |
| return null; |
| } |
| return _getConstructorSuggestion( |
| candidate, |
| request, |
| libraryUriStr, |
| isNotImportedLibrary, |
| requiredImports, |
| ); |
| case EnumConstantSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case EnumSuggestion(): |
| return _getInterfaceSuggestion( |
| candidate, |
| candidate.element, |
| request, |
| libraryUriStr, |
| isNotImportedLibrary, |
| requiredImports, |
| ); |
| case ExtensionSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| candidate.kind, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case ExtensionTypeSuggestion(): |
| return _getInterfaceSuggestion( |
| candidate, |
| candidate.element, |
| request, |
| libraryUriStr, |
| isNotImportedLibrary, |
| requiredImports, |
| ); |
| case FormalParameterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case FunctionCall(): |
| return CompletionSuggestion( |
| CompletionSuggestionKind.INVOCATION, |
| candidate.relevanceScore, |
| e.MethodElement.CALL_METHOD_NAME, |
| e.MethodElement.CALL_METHOD_NAME.length, |
| 0, |
| false, |
| false, |
| displayText: candidate.completion, |
| element: _getElementForFunctionCall(), |
| returnType: 'void', |
| parameterNames: [], |
| parameterTypes: [], |
| requiredParameterCount: 0, |
| hasNamedParameters: false, |
| ); |
| case IdentifierSuggestion(): |
| return CompletionSuggestion( |
| CompletionSuggestionKind.IDENTIFIER, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| ); |
| case ImportPrefixSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| |
| case KeywordSuggestion(): |
| return CompletionSuggestion( |
| CompletionSuggestionKind.KEYWORD, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| ); |
| case LabelSuggestion(): |
| var completion = candidate.label.label.name; |
| var suggestion = CompletionSuggestion( |
| CompletionSuggestionKind.IDENTIFIER, |
| candidate.relevanceScore, |
| completion, |
| completion.length, |
| 0, |
| false, |
| false, |
| ); |
| suggestion.element = createLocalElement( |
| request.source, |
| ElementKind.LABEL, |
| candidate.label.label, |
| ); |
| return suggestion; |
| case LoadLibraryFunctionSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| candidate.kind, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| |
| case LocalFunctionSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| candidate.kind, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| |
| case LocalVariableSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case MixinSuggestion(): |
| return _getInterfaceSuggestion( |
| candidate, |
| candidate.element, |
| request, |
| libraryUriStr, |
| isNotImportedLibrary, |
| requiredImports, |
| ); |
| case NamedArgumentSuggestion(): |
| return _getNamedArgumentSuggestion(candidate, request); |
| case NameSuggestion(): |
| var name = candidate.completion; |
| return CompletionSuggestion( |
| CompletionSuggestionKind.IDENTIFIER, |
| candidate.relevanceScore, |
| name, |
| name.length, |
| 0, |
| false, |
| false, |
| ); |
| case OverrideSuggestion(): |
| return await _getOverrideSuggestion(candidate, request); |
| case RecordLiteralNamedFieldSuggestion(): |
| var field = candidate.field; |
| return CompletionSuggestion( |
| CompletionSuggestionKind.NAMED_ARGUMENT, |
| Relevance.requiredNamedArgument, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| parameterName: field.name, |
| parameterType: field.type.getDisplayString(), |
| ); |
| case SetterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case StaticFieldSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case SuperParameterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TopLevelFunctionSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| candidate.kind, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TopLevelGetterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TopLevelSetterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TopLevelVariableSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TypeAliasSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case TypeParameterSuggestion(): |
| return _getDartCompletionSuggestion( |
| candidate.element, |
| candidate.completion, |
| candidate.relevanceScore, |
| CompletionSuggestionKind.IDENTIFIER, |
| request, |
| isNotImportedLibrary, |
| libraryUriStr, |
| requiredImports, |
| ); |
| case UriSuggestion(): |
| var uri = candidate.uriStr; |
| return CompletionSuggestion( |
| CompletionSuggestionKind.IMPORT, |
| candidate.relevanceScore, |
| uri, |
| uri.length, |
| 0, |
| false, |
| false, |
| ); |
| } |
| } |
| |
| _ParameterData _createParameterData(e.Element element) { |
| List<String>? parameterNames; |
| List<String>? parameterTypes; |
| int? requiredParameterCount; |
| bool? hasNamedParameters; |
| CompletionDefaultArgumentList? defaultArgumentList; |
| if (element is e.ExecutableElement && element is! e.PropertyAccessorElement) { |
| parameterNames = |
| element.formalParameters.map((parameter) { |
| return parameter.displayName; |
| }).toList(); |
| parameterTypes = |
| element.formalParameters.map((e.FormalParameterElement parameter) { |
| return parameter.type.getDisplayString(); |
| }).toList(); |
| |
| var requiredParameters = element.formalParameters.where( |
| (e.FormalParameterElement param) => param.isRequiredPositional, |
| ); |
| requiredParameterCount = requiredParameters.length; |
| |
| var namedParameters = element.formalParameters.where( |
| (e.FormalParameterElement param) => param.isNamed, |
| ); |
| hasNamedParameters = namedParameters.isNotEmpty; |
| |
| defaultArgumentList = computeCompletionDefaultArgumentList( |
| element, |
| requiredParameters, |
| namedParameters, |
| ); |
| } |
| return _ParameterData( |
| parameterNames, |
| parameterTypes, |
| requiredParameterCount, |
| hasNamedParameters, |
| defaultArgumentList, |
| ); |
| } |
| |
| CompletionSuggestion? _getConstructorSuggestion( |
| ConstructorSuggestion candidate, |
| DartCompletionRequest request, |
| String? libraryUriStr, |
| bool? isNotImported, |
| List<Uri> requiredImports, |
| ) { |
| var hasClassName = candidate.hasClassName; |
| var prefix = candidate.prefix; |
| var completion = candidate.completion; |
| // If the class name is already in the text, then we don't support |
| // prepending a prefix. |
| assert(!hasClassName || prefix == null); |
| if (prefix != null) { |
| completion = '$prefix.$completion'; |
| } |
| |
| var constructor = candidate.element; |
| |
| var suggestedElement = convertElement(constructor); |
| var parameterData = _createParameterData(constructor); |
| |
| var suggestion = DartCompletionSuggestion( |
| candidate.kind, |
| candidate.relevanceScore, |
| completion, |
| completion.length /*selectionOffset*/, |
| 0 /*selectionLength*/, |
| constructor.hasOrInheritsDeprecated, |
| false /*isPotential*/, |
| element: suggestedElement, |
| declaringType: _getDeclaringType(constructor), |
| returnType: getReturnTypeString(constructor), |
| requiredParameterCount: parameterData.requiredParameterCount, |
| hasNamedParameters: parameterData.hasNamedParameters, |
| parameterNames: parameterData.parameterNames, |
| parameterTypes: parameterData.parameterTypes, |
| defaultArgumentListString: parameterData.defaultArgumentList?.text, |
| defaultArgumentListTextRanges: parameterData.defaultArgumentList?.ranges, |
| libraryUri: libraryUriStr, |
| isNotImported: isNotImported, |
| requiredImports: requiredImports, |
| colorHex: getColorHexString(constructor), |
| ); |
| _setDocumentation(suggestion, constructor, request); |
| return suggestion; |
| } |
| |
| DartCompletionSuggestion _getDartCompletionSuggestion( |
| e.Element element, |
| String completion, |
| int relevance, |
| CompletionSuggestionKind kind, |
| DartCompletionRequest request, |
| bool? isNotImported, |
| String? libraryUriStr, |
| List<Uri> requiredImports, { |
| String? displayString, |
| }) { |
| var suggestedElement = convertElement(element); |
| _ParameterData? parameterData; |
| if (element is e.ExecutableElement && element is! e.PropertyAccessorElement) { |
| parameterData = _createParameterData(element); |
| } |
| |
| var suggestion = DartCompletionSuggestion( |
| kind, |
| relevance, |
| completion, |
| completion.length /*selectionOffset*/, |
| 0 /*selectionLength*/, |
| element.hasOrInheritsDeprecated, |
| false /*isPotential*/, |
| element: suggestedElement, |
| declaringType: _getDeclaringType(element), |
| returnType: getReturnTypeString(element), |
| requiredParameterCount: parameterData?.requiredParameterCount, |
| hasNamedParameters: parameterData?.hasNamedParameters, |
| parameterNames: parameterData?.parameterNames, |
| parameterTypes: parameterData?.parameterTypes, |
| defaultArgumentListString: parameterData?.defaultArgumentList?.text, |
| defaultArgumentListTextRanges: parameterData?.defaultArgumentList?.ranges, |
| libraryUri: libraryUriStr, |
| isNotImported: isNotImported, |
| requiredImports: requiredImports, |
| colorHex: getColorHexString(element), |
| displayText: displayString, |
| ); |
| _setDocumentation(suggestion, element, request); |
| return suggestion; |
| } |
| |
| String? _getDeclaringType(e.Element element) { |
| String? declaringType; |
| if (element is! e.FormalParameterElement) { |
| var enclosingElement = element.enclosingElement; |
| |
| if (enclosingElement is e.InterfaceElement) { |
| declaringType = enclosingElement.displayName; |
| } |
| } |
| return declaringType; |
| } |
| |
| Element _getElementForFunctionCall() { |
| return Element( |
| ElementKind.METHOD, |
| e.MethodElement.CALL_METHOD_NAME, |
| Element.makeFlags(), |
| parameters: '()', |
| returnType: 'void', |
| ); |
| } |
| |
| CompletionSuggestion _getInterfaceSuggestion( |
| CandidateSuggestion candidate, |
| e.Element interfaceElement, |
| DartCompletionRequest request, |
| String? libraryUriStr, |
| bool? isNotImported, |
| List<Uri> requiredImports, |
| ) { |
| var suggestedElement = convertElement(interfaceElement); |
| var completion = candidate.completion; |
| var suggestion = DartCompletionSuggestion( |
| CompletionSuggestionKind.IDENTIFIER, |
| candidate.relevanceScore, |
| completion, |
| completion.length /*selectionOffset*/, |
| 0 /*selectionLength*/, |
| interfaceElement.hasOrInheritsDeprecated, |
| false /*isPotential*/, |
| element: suggestedElement, |
| declaringType: _getDeclaringType(interfaceElement), |
| returnType: getReturnTypeString(interfaceElement), |
| libraryUri: libraryUriStr, |
| isNotImported: isNotImported, |
| requiredImports: requiredImports, |
| colorHex: getColorHexString(interfaceElement), |
| ); |
| |
| _setDocumentation(suggestion, interfaceElement, request); |
| return suggestion; |
| } |
| |
| CompletionSuggestion _getNamedArgumentSuggestion( |
| NamedArgumentSuggestion candidate, |
| DartCompletionRequest request, |
| ) { |
| var parameter = candidate.parameter; |
| var name = parameter.name; |
| var type = parameter.type.getDisplayString(); |
| |
| var suggestion = DartCompletionSuggestion( |
| CompletionSuggestionKind.NAMED_ARGUMENT, |
| candidate.relevanceScore, |
| candidate.completion, |
| candidate.selectionOffset, |
| 0, |
| false, |
| false, |
| parameterName: name, |
| parameterType: type, |
| replacementLength: candidate.replacementLength, |
| element: convertElement(parameter), |
| ); |
| |
| _setDocumentation(suggestion, parameter, request); |
| return suggestion; |
| } |
| |
| /// 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<CompletionSuggestion?> _getOverrideSuggestion( |
| OverrideSuggestion candidate, |
| DartCompletionRequest request, |
| ) async { |
| var displayTextBuffer = StringBuffer(); |
| var overrideImports = <Uri>{}; |
| var builder = ChangeBuilder(session: request.analysisSession); |
| var replacementRange = candidate.replacementRange; |
| var element = candidate.element; |
| |
| await builder.addDartFileEdit(request.path, createEditsForImports: false, ( |
| builder, |
| ) { |
| builder.addReplacement(replacementRange, (builder) { |
| builder.writeOverride( |
| element, |
| displayTextBuffer: displayTextBuffer, |
| invokeSuper: candidate.shouldInvokeSuper, |
| ); |
| }); |
| overrideImports.addAll(builder.requiredImports); |
| }); |
| |
| var fileEdits = builder.sourceChange.edits; |
| if (fileEdits.length != 1) { |
| return null; |
| } |
| |
| var sourceEdits = fileEdits[0].edits; |
| if (sourceEdits.length != 1) { |
| return null; |
| } |
| |
| 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 (candidate.skipAt && completion.startsWith(overrideAnnotation)) { |
| completion = completion.substring('@'.length); |
| } |
| if (completion.isEmpty) { |
| return null; |
| } |
| |
| var selectionRange = builder.selectionRange; |
| if (selectionRange == null) { |
| return null; |
| } |
| var offsetDelta = replacementRange.offset + replacement.indexOf(completion); |
| |
| var displayText = displayTextBuffer.toString(); |
| if (displayText.isEmpty) { |
| return null; |
| } |
| |
| if (candidate.skipAt) { |
| displayText = 'override $displayText'; |
| } |
| |
| var suggestion = DartCompletionSuggestion( |
| CompletionSuggestionKind.OVERRIDE, |
| candidate.relevanceScore, |
| completion, |
| selectionRange.offset - offsetDelta, |
| selectionRange.length, |
| element.metadata.hasDeprecated, |
| false, |
| displayText: displayText, |
| requiredImports: overrideImports.toList(), |
| ); |
| suggestion.element = convertElement(element); |
| return suggestion; |
| } |
| |
| /// If the [element] has a documentation comment, fill the [suggestion]'s |
| /// documentation fields. |
| void _setDocumentation( |
| CompletionSuggestion suggestion, |
| e.Element element, |
| DartCompletionRequest request, |
| ) { |
| var doc = request.documentationComputer.compute( |
| element, |
| includeSummary: true, |
| ); |
| if (doc is DocumentationWithSummary) { |
| suggestion.docComplete = doc.full; |
| suggestion.docSummary = doc.summary; |
| } |
| } |
| |
| class _ParameterData { |
| List<String>? parameterNames; |
| List<String>? parameterTypes; |
| int? requiredParameterCount; |
| bool? hasNamedParameters; |
| CompletionDefaultArgumentList? defaultArgumentList; |
| |
| _ParameterData( |
| this.parameterNames, |
| this.parameterTypes, |
| this.requiredParameterCount, |
| this.hasNamedParameters, |
| this.defaultArgumentList, |
| ); |
| } |