| // Copyright (c) 2019, 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/lsp_protocol/protocol.dart' hide Element; |
| import 'package:analysis_server/src/computer/computer_hover.dart'; |
| import 'package:analysis_server/src/lsp/client_capabilities.dart'; |
| import 'package:analysis_server/src/lsp/constants.dart'; |
| import 'package:analysis_server/src/lsp/handlers/handlers.dart'; |
| import 'package:analysis_server/src/lsp/mapping.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/analysis/session.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| |
| class CompletionResolveHandler |
| extends MessageHandler<CompletionItem, CompletionItem> { |
| /// The last completion item we asked to be resolved. |
| /// |
| /// Used to abort previous requests in async handlers if another resolve request |
| /// arrives while the previous is being processed (for clients that don't send |
| /// cancel events). |
| CompletionItem? _latestCompletionItem; |
| |
| CompletionResolveHandler(super.server); |
| |
| @override |
| Method get handlesMessage => Method.completionItem_resolve; |
| |
| @override |
| LspJsonHandler<CompletionItem> get jsonHandler => CompletionItem.jsonHandler; |
| |
| @override |
| Future<ErrorOr<CompletionItem>> handle( |
| CompletionItem params, |
| MessageInfo message, |
| CancellationToken token, |
| ) async { |
| final resolutionInfo = params.data; |
| |
| if (resolutionInfo is DartSuggestionSetCompletionItemResolutionInfo) { |
| return resolveDartSuggestionSetCompletion(params, resolutionInfo, token); |
| } else if (resolutionInfo is DartNotImportedCompletionResolutionInfo) { |
| return resolveDartNotImportedCompletion(params, resolutionInfo, token); |
| } else if (resolutionInfo is PubPackageCompletionItemResolutionInfo) { |
| return resolvePubPackageCompletion(params, resolutionInfo, token); |
| } else { |
| return success(params); |
| } |
| } |
| |
| Future<ErrorOr<CompletionItem>> resolveDartCompletion( |
| CompletionItem item, |
| LspClientCapabilities clientCapabilities, |
| CancellationToken token, { |
| required String file, |
| required Uri libraryUri, |
| }) async { |
| const timeout = Duration(milliseconds: 1000); |
| var timer = Stopwatch()..start(); |
| _latestCompletionItem = item; |
| while (item == _latestCompletionItem && timer.elapsed < timeout) { |
| try { |
| final session = await server.getAnalysisSession(file); |
| |
| // We shouldn't not get a driver/session, but if we did perhaps the file |
| // was removed from the analysis set so assume the request is no longer |
| // valid. |
| if (session == null || token.isCancellationRequested) { |
| return cancelled(); |
| } |
| |
| final result = await session.getResolvedUnit(file); |
| if (result is! ResolvedUnitResult) { |
| return cancelled(); |
| } |
| |
| final element = await _getElement(session, libraryUri, item); |
| if (element == null) { |
| return error( |
| ErrorCodes.InvalidParams, |
| 'No such element: ${item.label} in $libraryUri', |
| item.label, |
| ); |
| } |
| |
| if (token.isCancellationRequested) { |
| return cancelled(); |
| } |
| |
| final builder = ChangeBuilder(session: session); |
| await builder.addDartFileEdit(file, (builder) { |
| builder.importLibraryElement(libraryUri); |
| }); |
| |
| if (token.isCancellationRequested) { |
| return cancelled(); |
| } |
| |
| final changes = builder.sourceChange; |
| final thisFilesChanges = |
| changes.edits.where((e) => e.file == file).toList(); |
| final otherFilesChanges = |
| changes.edits.where((e) => e.file != file).toList(); |
| |
| // If this completion involves editing other files, we'll need to build |
| // a command that the client will call to apply those edits later. |
| Command? command; |
| if (otherFilesChanges.isNotEmpty) { |
| final workspaceEdit = |
| createPlainWorkspaceEdit(server, otherFilesChanges); |
| command = Command( |
| title: 'Add import', |
| command: Commands.sendWorkspaceEdit, |
| arguments: [ |
| {'edit': workspaceEdit} |
| ]); |
| } |
| |
| final formats = clientCapabilities.completionDocumentationFormats; |
| final dartDocInfo = server.getDartdocDirectiveInfoForSession(session); |
| final dartDocData = |
| DartUnitHoverComputer.computeDocumentation(dartDocInfo, element); |
| final dartDoc = dartDocData?.full; |
| // `dartDoc` can be both null or empty. |
| final documentation = dartDoc != null && dartDoc.isNotEmpty |
| ? asMarkupContentOrString(formats, dartDoc) |
| : null; |
| |
| // If the only URI we have is a file:// URI, display it as relative to |
| // the file we're importing into, rather than the full URI. |
| final pathContext = server.resourceProvider.pathContext; |
| final autoImportDisplayUri = libraryUri.isScheme('file') |
| // Compute the relative path and then put into a URI so the display |
| // always uses forward slashes (as a URI) regardless of platform. |
| ? Uri.file(pathContext.relative( |
| libraryUri.toFilePath(), |
| from: pathContext.dirname(file), |
| )) |
| : libraryUri; |
| |
| return success(CompletionItem( |
| label: item.label, |
| kind: item.kind, |
| tags: item.tags, |
| detail: changes.edits.isNotEmpty |
| ? "Auto import from '$autoImportDisplayUri'\n\n${item.detail ?? ''}" |
| .trim() |
| : item.detail, |
| documentation: documentation, |
| deprecated: item.deprecated, |
| preselect: item.preselect, |
| sortText: item.sortText, |
| filterText: item.filterText, |
| insertTextFormat: item.insertTextFormat, |
| insertTextMode: item.insertTextMode, |
| textEdit: item.textEdit, |
| additionalTextEdits: thisFilesChanges |
| .expand((change) => |
| change.edits.map((edit) => toTextEdit(result.lineInfo, edit))) |
| .toList(), |
| commitCharacters: item.commitCharacters, |
| command: command ?? item.command, |
| data: item.data, |
| )); |
| } on InconsistentAnalysisException { |
| // Loop around to try again. |
| } |
| } |
| |
| // Timeout or abort, send the empty response. |
| |
| return error( |
| ErrorCodes.RequestCancelled, |
| 'Request was cancelled for taking too long or another request being received', |
| null, |
| ); |
| } |
| |
| Future<ErrorOr<CompletionItem>> resolveDartNotImportedCompletion( |
| CompletionItem item, |
| DartNotImportedCompletionResolutionInfo data, |
| CancellationToken token, |
| ) async { |
| final clientCapabilities = server.clientCapabilities; |
| if (clientCapabilities == null) { |
| // This should not happen unless a client misbehaves. |
| return error(ErrorCodes.ServerNotInitialized, |
| 'Requests not before server is initilized'); |
| } |
| |
| return resolveDartCompletion( |
| item, |
| clientCapabilities, |
| token, |
| file: data.file, |
| libraryUri: Uri.parse(data.libraryUri), |
| ); |
| } |
| |
| Future<ErrorOr<CompletionItem>> resolveDartSuggestionSetCompletion( |
| CompletionItem item, |
| DartSuggestionSetCompletionItemResolutionInfo data, |
| CancellationToken token, |
| ) async { |
| final clientCapabilities = server.clientCapabilities; |
| if (clientCapabilities == null) { |
| // This should not happen unless a client misbehaves. |
| return serverNotInitializedError; |
| } |
| |
| var library = server.declarationsTracker?.getLibrary(data.libId); |
| if (library == null) { |
| return error( |
| ErrorCodes.InvalidParams, |
| 'Library ID is not valid: ${data.libId}', |
| data.libId.toString(), |
| ); |
| } |
| |
| return resolveDartCompletion( |
| item, |
| clientCapabilities, |
| token, |
| file: data.file, |
| libraryUri: library.uri, |
| ); |
| } |
| |
| Future<ErrorOr<CompletionItem>> resolvePubPackageCompletion( |
| CompletionItem item, |
| PubPackageCompletionItemResolutionInfo data, |
| CancellationToken token, |
| ) async { |
| // Fetch details for this package. This may come from the cache or trigger |
| // a real web request to the Pub API. |
| final packageDetails = |
| await server.pubPackageService.packageDetails(data.packageName); |
| |
| if (token.isCancellationRequested) { |
| return cancelled(); |
| } |
| |
| final description = packageDetails?.description; |
| return success(CompletionItem( |
| label: item.label, |
| kind: item.kind, |
| tags: item.tags, |
| detail: item.detail, |
| documentation: description != null |
| ? Either2<MarkupContent, String>.t2(description) |
| : null, |
| deprecated: item.deprecated, |
| preselect: item.preselect, |
| sortText: item.sortText, |
| filterText: item.filterText, |
| insertTextFormat: item.insertTextFormat, |
| textEdit: item.textEdit, |
| additionalTextEdits: item.additionalTextEdits, |
| commitCharacters: item.commitCharacters, |
| command: item.command, |
| data: item.data, |
| )); |
| } |
| |
| /// Gets the [Element] for the completion item [item] in [libraryUri]. |
| Future<Element?> _getElement( |
| AnalysisSession session, |
| Uri libraryUri, |
| CompletionItem item, |
| ) async { |
| // If filterText is different to the label, it's because label has |
| // parens/args appended so we should take the filterText to get the |
| // elements name without. We cannot use insertText as it may include |
| // snippets, whereas filterText is always just the pure string. |
| var name = item.filterText ?? item.label; |
| |
| // The label might be `MyEnum.myValue`, but we need to find `MyEnum`. |
| if (name.contains('.')) { |
| name = name.substring(0, name.indexOf('.')); |
| } |
| |
| // TODO(dantup): This is not handling default constructors or enums |
| // correctly, so they will both show dart docs from the class/enum and not |
| // the constructor/enum member. |
| |
| final result = await session.getLibraryByUri(libraryUri.toString()); |
| return result is LibraryElementResult |
| ? result.element.exportNamespace.get(name) |
| : null; |
| } |
| } |