Wire up LSP completion resolution provider + sendWorkspaceEdit command
Change-Id: I424b4d9bbc64f6fea7a4d9b06e2180b3fbd21f86
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102703
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index 3f03e09..bfa8c77 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -14,6 +14,7 @@
import 'dart:core' hide deprecated;
import 'dart:core' as core show deprecated;
import 'dart:convert' show JsonEncoder;
+
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart'
show listEqual, mapEqual;
@@ -69,6 +70,89 @@
String toString() => jsonEncoder.convert(toJson());
}
+class CompletionItemResolutionInfo implements ToJsonable {
+ static const jsonHandler = const LspJsonHandler(
+ CompletionItemResolutionInfo.canParse,
+ CompletionItemResolutionInfo.fromJson);
+
+ CompletionItemResolutionInfo(
+ this.file, this.offset, this.libraryId, this.autoImportDisplayUri) {
+ if (file == null) {
+ throw 'file is required but was not provided';
+ }
+ if (offset == null) {
+ throw 'offset is required but was not provided';
+ }
+ if (libraryId == null) {
+ throw 'libraryId is required but was not provided';
+ }
+ if (autoImportDisplayUri == null) {
+ throw 'autoImportDisplayUri is required but was not provided';
+ }
+ }
+ static CompletionItemResolutionInfo fromJson(Map<String, dynamic> json) {
+ final file = json['file'];
+ final offset = json['offset'];
+ final libraryId = json['libraryId'];
+ final autoImportDisplayUri = json['autoImportDisplayUri'];
+ return new CompletionItemResolutionInfo(
+ file, offset, libraryId, autoImportDisplayUri);
+ }
+
+ final String autoImportDisplayUri;
+ final String file;
+ final num libraryId;
+ final num offset;
+
+ Map<String, dynamic> toJson() {
+ Map<String, dynamic> __result = {};
+ __result['file'] = file ?? (throw 'file is required but was not set');
+ __result['offset'] = offset ?? (throw 'offset is required but was not set');
+ __result['libraryId'] =
+ libraryId ?? (throw 'libraryId is required but was not set');
+ __result['autoImportDisplayUri'] = autoImportDisplayUri ??
+ (throw 'autoImportDisplayUri is required but was not set');
+ return __result;
+ }
+
+ static bool canParse(Object obj) {
+ return obj is Map<String, dynamic> &&
+ obj.containsKey('file') &&
+ obj['file'] is String &&
+ obj.containsKey('offset') &&
+ obj['offset'] is num &&
+ obj.containsKey('libraryId') &&
+ obj['libraryId'] is num &&
+ obj.containsKey('autoImportDisplayUri') &&
+ obj['autoImportDisplayUri'] is String;
+ }
+
+ @override
+ bool operator ==(other) {
+ if (other is CompletionItemResolutionInfo) {
+ return file == other.file &&
+ offset == other.offset &&
+ libraryId == other.libraryId &&
+ autoImportDisplayUri == other.autoImportDisplayUri &&
+ true;
+ }
+ return false;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 0;
+ hash = JenkinsSmiHash.combine(hash, file.hashCode);
+ hash = JenkinsSmiHash.combine(hash, offset.hashCode);
+ hash = JenkinsSmiHash.combine(hash, libraryId.hashCode);
+ hash = JenkinsSmiHash.combine(hash, autoImportDisplayUri.hashCode);
+ return JenkinsSmiHash.finish(hash);
+ }
+
+ @override
+ String toString() => jsonEncoder.convert(toJson());
+}
+
class DartDiagnosticServer implements ToJsonable {
static const jsonHandler = const LspJsonHandler(
DartDiagnosticServer.canParse, DartDiagnosticServer.fromJson);
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index 7706a53..8ae18ae 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -14,6 +14,7 @@
import 'dart:core' hide deprecated;
import 'dart:core' as core show deprecated;
import 'dart:convert' show JsonEncoder;
+import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/protocol/protocol_internal.dart'
show listEqual, mapEqual;
@@ -1510,7 +1511,9 @@
json['commitCharacters']?.map((item) => item)?.cast<String>()?.toList();
final command =
json['command'] != null ? Command.fromJson(json['command']) : null;
- final data = json['data'];
+ final data = json['data'] != null
+ ? CompletionItemResolutionInfo.fromJson(json['data'])
+ : null;
return new CompletionItem(
label,
kind,
@@ -1551,7 +1554,7 @@
/// A data entry field that is preserved on a completion item between a
/// completion and a completion resolve request.
- final dynamic data;
+ final CompletionItemResolutionInfo data;
/// Indicates if this item is deprecated.
final bool deprecated;
@@ -1684,7 +1687,8 @@
(obj['commitCharacters'] is List &&
(obj['commitCharacters'].every((item) => item is String)))) &&
(obj['command'] == null || Command.canParse(obj['command'])) &&
- (obj['data'] == null || true);
+ (obj['data'] == null ||
+ CompletionItemResolutionInfo.canParse(obj['data']));
}
@override
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index b5922b0..904feeb 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -9,9 +9,14 @@
/// A list of all commands IDs that can be sent to the client to inform which
/// commands should be sent to the server for execution (as opposed to being
/// executed in the local plugin).
- static const serverSupportedCommands = [sortMembers, organizeImports];
+ static const serverSupportedCommands = [
+ sortMembers,
+ organizeImports,
+ sendWorkspaceEdit,
+ ];
static const sortMembers = 'edit.sortMembers';
static const organizeImports = 'edit.organizeImports';
+ static const sendWorkspaceEdit = 'edit.sendWorkspaceEdit';
}
/// CodeActionKinds supported by the server that are not declared in the LSP spec.
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart
new file mode 100644
index 0000000..99b061f
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart
@@ -0,0 +1,42 @@
+// 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 'dart:async';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/lsp/constants.dart';
+import 'package:analysis_server/src/lsp/handlers/commands/simple_edit_handler.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
+
+/// This command allows a client to request the server send it a
+/// workspace/applyEdit command, simply passing through the edits provided
+/// by the client. This is to handle completion items that need to make edits
+/// in files other than those containing the completion (not natively supported
+/// by LSP). The edits are put into the [CompletionItem]s command field/
+/// args and when the client calls the server to execute that command, the server
+/// will call the client to execute workspace/applyEdit.
+class SendWorkspaceEditCommandHandler extends SimpleEditCommandHandler {
+ SendWorkspaceEditCommandHandler(LspAnalysisServer server) : super(server);
+
+ @override
+ String get commandName => 'Send Workspace Edit';
+
+ @override
+ Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+ if (arguments == null ||
+ arguments.length != 1 ||
+ arguments[0] is! Map<String, dynamic>) {
+ return ErrorOr.error(new ResponseError(
+ ServerErrorCodes.InvalidCommandArguments,
+ '$commandName requires a single List argument of WorkspaceEdit',
+ null,
+ ));
+ }
+
+ final workspaceEdit = WorkspaceEdit.fromJson(arguments[0]);
+
+ return await sendWorkspaceEditToClient(workspaceEdit);
+ }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index 147b48f..e4c9b61 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -41,7 +41,11 @@
class CompletionHandler
extends MessageHandler<CompletionParams, List<CompletionItem>> {
- CompletionHandler(LspAnalysisServer server) : super(server);
+ final bool suggestFromUnimportedLibraries;
+ CompletionHandler(
+ LspAnalysisServer server, this.suggestFromUnimportedLibraries)
+ : super(server);
+
Method get handlesMessage => Method.textDocument_completion;
@override
@@ -62,6 +66,9 @@
completionCapabilities.completionItemKind.valueSet)
: defaultSupportedCompletionKinds;
+ final includeSuggestionSets = suggestFromUnimportedLibraries &&
+ server?.clientCapabilities?.workspace?.applyEdit == true;
+
final pos = params.position;
final path = pathOfDoc(params.textDocument);
final unit = await path.mapResult(requireResolvedUnit);
@@ -69,6 +76,7 @@
return offset.mapResult((offset) => _getItems(
completionCapabilities,
clientSupportedCompletionKinds,
+ includeSuggestionSets,
unit.result,
offset,
));
@@ -77,6 +85,7 @@
Future<ErrorOr<List<CompletionItem>>> _getItems(
TextDocumentClientCapabilitiesCompletion completionCapabilities,
HashSet<CompletionItemKind> clientSupportedCompletionKinds,
+ bool includeSuggestionSets,
ResolvedUnitResult unit,
int offset,
) async {
@@ -90,14 +99,11 @@
try {
CompletionContributor contributor = new DartCompletionManager();
- final items = await contributor.computeSuggestions(completionRequest);
- performance.notificationCount = 1;
- performance.suggestionCountFirst = items.length;
- performance.suggestionCountLast = items.length;
- performance.complete();
+ final suggestions =
+ await contributor.computeSuggestions(completionRequest);
- return success(items
+ final results = suggestions
.map((item) => toCompletionItem(
completionCapabilities,
clientSupportedCompletionKinds,
@@ -106,7 +112,14 @@
completionRequest.replacementOffset,
completionRequest.replacementLength,
))
- .toList());
+ .toList();
+
+ performance.notificationCount = 1;
+ performance.suggestionCountFirst = results.length;
+ performance.suggestionCountLast = results.length;
+ performance.complete();
+
+ return success(results);
} on AbortCompletion {
return success([]);
}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
new file mode 100644
index 0000000..c32ec91
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
@@ -0,0 +1,25 @@
+// 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 'dart:async';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
+
+class CompletionResolveHandler
+ extends MessageHandler<CompletionItem, CompletionItem> {
+ CompletionResolveHandler(LspAnalysisServer server) : super(server);
+
+ Method get handlesMessage => Method.completionItem_resolve;
+
+ @override
+ LspJsonHandler<CompletionItem> get jsonHandler => CompletionItem.jsonHandler;
+
+ Future<ErrorOr<CompletionItem>> handle(CompletionItem item) async {
+ // TODO: Implement resolution. For now just always return the same item back.
+ return success(item);
+ }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
index b40f4b4..c440afe 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
@@ -8,6 +8,7 @@
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/commands/organize_imports.dart';
+import 'package:analysis_server/src/lsp/handlers/commands/send_workspace_edit.dart';
import 'package:analysis_server/src/lsp/handlers/commands/sort_members.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
@@ -21,6 +22,8 @@
: commandHandlers = {
Commands.sortMembers: new SortMembersCommandHandler(server),
Commands.organizeImports: new OrganizeImportsCommandHandler(server),
+ Commands.sendWorkspaceEdit:
+ new SendWorkspaceEditCommandHandler(server),
},
super(server);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
index 0e1a27b..ea1ed19 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
@@ -31,6 +31,13 @@
true
: false;
+ // The suggestFromUnimportedLibraries flag allows clients to opt-out of
+ // behaviour of including suggestions that are not imported. Defaults to true,
+ // so must be explicitly passed as false to disable.
+ final suggestFromUnimportedLibraries = params.initializationOptions ==
+ null ||
+ params.initializationOptions['suggestFromUnimportedLibraries'] != false;
+
if (!onlyAnalyzeProjectsWithOpenFiles) {
if (params.workspaceFolders != null) {
params.workspaceFolders.forEach((wf) {
@@ -50,7 +57,11 @@
server.handleClientConnection(params.capabilities);
server.messageHandler = new InitializingStateMessageHandler(
- server, openWorkspacePaths, onlyAnalyzeProjectsWithOpenFiles);
+ server,
+ openWorkspacePaths,
+ onlyAnalyzeProjectsWithOpenFiles,
+ suggestFromUnimportedLibraries,
+ );
final codeActionLiteralSupport =
params.capabilities.textDocument?.codeAction?.codeActionLiteralSupport;
@@ -68,7 +79,7 @@
)),
true, // hoverProvider
new CompletionOptions(
- false,
+ true, // resolveProvider
// Set the characters that will cause the editor to automatically
// trigger completion.
// TODO(dantup): There are several characters that we want to conditionally
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
index 463ac9e..ea74fc5 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
@@ -10,10 +10,13 @@
class IntializedMessageHandler extends MessageHandler<InitializedParams, void> {
final List<String> openWorkspacePaths;
- final bool onlyAnalyzeProjectsWithOpenFiles;
- IntializedMessageHandler(LspAnalysisServer server, this.openWorkspacePaths,
- this.onlyAnalyzeProjectsWithOpenFiles)
- : super(server);
+ final bool onlyAnalyzeProjectsWithOpenFiles, suggestFromUnimportedLibraries;
+ IntializedMessageHandler(
+ LspAnalysisServer server,
+ this.openWorkspacePaths,
+ this.onlyAnalyzeProjectsWithOpenFiles,
+ this.suggestFromUnimportedLibraries,
+ ) : super(server);
Method get handlesMessage => Method.initialized;
@override
@@ -22,7 +25,10 @@
ErrorOr<void> handle(InitializedParams params) {
server.messageHandler = new InitializedStateMessageHandler(
- server, onlyAnalyzeProjectsWithOpenFiles);
+ server,
+ onlyAnalyzeProjectsWithOpenFiles,
+ suggestFromUnimportedLibraries,
+ );
if (!onlyAnalyzeProjectsWithOpenFiles) {
server.setAnalysisRoots(openWorkspacePaths);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
index af5fd3d..60b6f53 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
@@ -10,6 +10,7 @@
import 'package:analysis_server/src/lsp/handlers/custom/handler_diagnostic_server.dart';
import 'package:analysis_server/src/lsp/handlers/handler_code_actions.dart';
import 'package:analysis_server/src/lsp/handlers/handler_completion.dart';
+import 'package:analysis_server/src/lsp/handlers/handler_completion_resolve.dart';
import 'package:analysis_server/src/lsp/handlers/handler_definition.dart';
import 'package:analysis_server/src/lsp/handlers/handler_document_highlights.dart';
import 'package:analysis_server/src/lsp/handlers/handler_document_symbols.dart';
@@ -51,6 +52,7 @@
InitializedStateMessageHandler(
LspAnalysisServer server,
bool onlyAnalyzeProjectsWithOpenFiles,
+ bool suggestFromUnimportedLibraries,
) : super(server) {
reject(Method.initialize, ServerErrorCodes.ServerAlreadyInitialized,
'Server already initialized');
@@ -66,7 +68,9 @@
new TextDocumentCloseHandler(server, onlyAnalyzeProjectsWithOpenFiles),
);
registerHandler(new HoverHandler(server));
- registerHandler(new CompletionHandler(server));
+ registerHandler(
+ new CompletionHandler(server, suggestFromUnimportedLibraries));
+ registerHandler(new CompletionResolveHandler(server));
registerHandler(new SignatureHelpHandler(server));
registerHandler(new DefinitionHandler(server));
registerHandler(new ReferencesHandler(server));
@@ -89,15 +93,22 @@
}
class InitializingStateMessageHandler extends ServerStateMessageHandler {
- InitializingStateMessageHandler(LspAnalysisServer server,
- List<String> openWorkspacePaths, bool onlyAnalyzeProjectsWithOpenFiles)
- : super(server) {
+ InitializingStateMessageHandler(
+ LspAnalysisServer server,
+ List<String> openWorkspacePaths,
+ bool onlyAnalyzeProjectsWithOpenFiles,
+ bool suggestFromUnimportedLibraries,
+ ) : super(server) {
reject(Method.initialize, ServerErrorCodes.ServerAlreadyInitialized,
'Server already initialized');
registerHandler(new ShutdownMessageHandler(server));
registerHandler(new ExitMessageHandler(server));
registerHandler(new IntializedMessageHandler(
- server, openWorkspacePaths, onlyAnalyzeProjectsWithOpenFiles));
+ server,
+ openWorkspacePaths,
+ onlyAnalyzeProjectsWithOpenFiles,
+ suggestFromUnimportedLibraries,
+ ));
}
@override
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index f0561e0..00cd091 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -84,6 +84,15 @@
final List<AstNode> customTypes = [
interface('DartDiagnosticServer', [field('port', type: 'number')]),
interface('AnalyzerStatusParams', [field('isAnalyzing', type: 'boolean')]),
+ interface(
+ 'CompletionItemResolutionInfo',
+ [
+ field('file', type: 'string'),
+ field('offset', type: 'number'),
+ field('libraryId', type: 'number'),
+ field('autoImportDisplayUri', type: 'string')
+ ],
+ ),
];
final String output = generateDartForTypes(customTypes);
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart
index afa5b76..130cbe2 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart
@@ -55,6 +55,7 @@
},
"CompletionItem": {
"kind": "CompletionItemKind",
+ "data": "CompletionItemResolutionInfo",
},
"DocumentHighlight": {
"kind": "DocumentHighlightKind",