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",