Add LSP tests for suggestion sets (marked failing)

Change-Id: Iab8f632038f136963ea2e02fa9e9d76c66e32845
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102702
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/test/lsp/completion_test.dart b/pkg/analysis_server/test/lsp/completion_test.dart
index f45fe3c..c910b60 100644
--- a/pkg/analysis_server/test/lsp/completion_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_test.dart
@@ -224,6 +224,212 @@
     expect(updated, contains('a.abcdefghij'));
   }
 
+  @failingTest
+  test_suggestionSets() async {
+    newFile(
+      join(projectFolderPath, 'other_file.dart'),
+      content: 'class InOtherFile {}',
+    );
+
+    final content = '''
+main() {
+  InOtherF^
+}
+    ''';
+
+    final initialAnalysis = waitForAnalysisComplete();
+    await initialize(
+        workspaceCapabilities:
+            withApplyEditSupport(emptyWorkspaceClientCapabilities));
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    // Find the completion for the class in the other file.
+    final completion = res.singleWhere((c) => c.label == 'InOtherFile');
+    expect(completion, isNotNull);
+
+    // Resolve the completion item (via server) to get its edits. This is the
+    // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to
+    // populate additional info (in our case, the additional edits for inserting
+    // the import).
+    final resolved = await resolveCompletion(completion);
+    expect(resolved, isNotNull);
+
+    // Ensure the detail field was update to show this will auto-import.
+    expect(
+        resolved.detail, startsWith("Auto import from '../other_file.dart'"));
+
+    // There should be no command for this item because it doesn't need imports
+    // in other files. Same-file completions are in additionalEdits.
+    expect(resolved.command, isNull);
+
+    // Apply both the main completion edit and the additionalTextEdits atomically.
+    final newContent = applyTextEdits(
+      withoutMarkers(content),
+      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+    );
+
+    // Ensure both edits were made - the completion, and the inserted import.
+    expect(newContent, equals('''
+import '../other_file.dart';
+
+main() {
+  InOtherFile
+}
+    '''));
+  }
+
+  @failingTest
+  test_suggestionSets_insertsIntoPartFiles() async {
+    // File we'll be adding an import for.
+    newFile(
+      join(projectFolderPath, 'other_file.dart'),
+      content: 'class InOtherFile {}',
+    );
+
+    // File that will have the import added.
+    final parentContent = '''part 'main.dart';''';
+    final parentFilePath = newFile(
+      join(projectFolderPath, 'lib', 'parent.dart'),
+      content: parentContent,
+    ).path;
+
+    // File that we're invoking completion in.
+    final content = '''
+part of 'parent.dart';
+main() {
+  InOtherF^
+}
+    ''';
+
+    final initialAnalysis = waitForAnalysisComplete();
+    await initialize(
+        workspaceCapabilities:
+            withApplyEditSupport(emptyWorkspaceClientCapabilities));
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    final completion = res.singleWhere((c) => c.label == 'InOtherFile');
+    expect(completion, isNotNull);
+
+    // Resolve the completion item to get its edits.
+    final resolved = await resolveCompletion(completion);
+    expect(resolved, isNotNull);
+    // Ensure it has a command, since it will need to make edits in other files
+    // and that's done by telling the server to send a workspace/applyEdit. LSP
+    // doesn't currently support these other-file edits in the completion.
+    // See https://github.com/microsoft/language-server-protocol/issues/749
+    expect(resolved.command, isNotNull);
+
+    // Apply all current-document edits.
+    final newContent = applyTextEdits(
+      withoutMarkers(content),
+      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+    );
+    expect(newContent, equals('''
+part of 'parent.dart';
+main() {
+  InOtherFile
+}
+    '''));
+
+    // Execute the associated command (which will handle edits in other files).
+    ApplyWorkspaceEditParams editParams;
+    final commandResponse = await handleExpectedRequest<Object,
+        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
+      Method.workspace_applyEdit,
+      () => executeCommand(resolved.command),
+      handler: (edit) {
+        // When the server sends the edit back, just keep a copy and say we
+        // applied successfully (it'll be verified below).
+        editParams = edit;
+        return new ApplyWorkspaceEditResponse(true, null);
+      },
+    );
+    // Successful edits return an empty success() response.
+    expect(commandResponse, isNull);
+
+    // Ensure the edit came back.
+    expect(editParams, isNotNull);
+    expect(editParams.edit.changes, isNotNull);
+
+    // Ensure applying the changes will give us the expected content.
+    final contents = {
+      parentFilePath: withoutMarkers(parentContent),
+    };
+    applyChanges(contents, editParams.edit.changes);
+
+    // Check the parent file was modified to include the import by the edits
+    // that came from the server.
+    expect(contents[parentFilePath], equals('''
+import '../other_file.dart';
+
+part 'main.dart';'''));
+  }
+
+  test_suggestionSets_unavailableIfDisabled() async {
+    newFile(
+      join(projectFolderPath, 'other_file.dart'),
+      content: 'class InOtherFile {}',
+    );
+
+    final content = '''
+main() {
+  InOtherF^
+}
+    ''';
+
+    final initialAnalysis = waitForAnalysisComplete();
+    // Support applyEdit, but explicitly disable the suggestions.
+    await initialize(
+      initializationOptions: {'suggestFromUnimportedLibraries': false},
+      workspaceCapabilities:
+          withApplyEditSupport(emptyWorkspaceClientCapabilities),
+    );
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    // Ensure the item doesn't appear in the results (because we might not
+    // be able to execute the import edits if they're in another file).
+    final completion = res.singleWhere(
+      (c) => c.label == 'InOtherFile',
+      orElse: () => null,
+    );
+    expect(completion, isNull);
+  }
+
+  test_suggestionSets_unavailableWithoutApplyEdit() async {
+    // If client doesn't advertise support for workspace/applyEdit, we won't
+    // include suggestion sets.
+    newFile(
+      join(projectFolderPath, 'other_file.dart'),
+      content: 'class InOtherFile {}',
+    );
+
+    final content = '''
+main() {
+  InOtherF^
+}
+    ''';
+
+    final initialAnalysis = waitForAnalysisComplete();
+    await initialize();
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    // Ensure the item doesn't appear in the results (because we might not
+    // be able to execute the import edits if they're in another file).
+    final completion = res.singleWhere(
+      (c) => c.label == 'InOtherFile',
+      orElse: () => null,
+    );
+    expect(completion, isNull);
+  }
+
   test_unopenFile() async {
     final content = '''
     class MyClass {
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index ace0a49..9c15da4 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -369,6 +369,14 @@
     return expectSuccessfulResponseTo<List<CompletionItem>>(request);
   }
 
+  Future<CompletionItem> resolveCompletion(CompletionItem item) {
+    final request = makeRequest(
+      Method.completionItem_resolve,
+      item,
+    );
+    return expectSuccessfulResponseTo<CompletionItem>(request);
+  }
+
   Future<List<Location>> getDefinition(Uri uri, Position pos) {
     final request = makeRequest(
       Method.textDocument_definition,