blob: 8a378f4198e1ee59ecae11aa6fc178f062f24fa4 [file] [log] [blame]
// Copyright (c) 2018, 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_generated.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import 'server_abstract.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(CompletionTest);
});
}
@reflectiveTest
class CompletionTest extends AbstractLspAnalysisServerTest {
test_completionKinds_default() async {
newFile(join(projectFolderPath, 'file.dart'));
newFolder(join(projectFolderPath, 'folder'));
final content = "import '^';";
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final file = res.singleWhere((c) => c.label == 'file.dart');
final folder = res.singleWhere((c) => c.label == 'folder/');
final builtin = res.singleWhere((c) => c.label == 'dart:core');
// Default capabilities include File + Module but not Folder.
expect(file.kind, equals(CompletionItemKind.File));
// We fall back to Module if Folder isn't supported.
expect(folder.kind, equals(CompletionItemKind.Module));
expect(builtin.kind, equals(CompletionItemKind.Module));
}
test_completionKinds_imports() async {
final content = "import '^';";
// Tell the server we support some specific CompletionItemKinds.
await initialize(
textDocumentCapabilities: withCompletionItemKinds(
emptyTextDocumentClientCapabilities,
[
CompletionItemKind.File,
CompletionItemKind.Folder,
CompletionItemKind.Module,
],
),
);
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final file = res.singleWhere((c) => c.label == 'file.dart');
final folder = res.singleWhere((c) => c.label == 'folder/');
final builtin = res.singleWhere((c) => c.label == 'dart:core');
expect(file.kind, equals(CompletionItemKind.File));
expect(folder.kind, equals(CompletionItemKind.Folder));
expect(builtin.kind, equals(CompletionItemKind.Module));
}
test_completionKinds_supportedSubset() async {
final content = '''
class MyClass {
String abcdefghij;
}
main() {
MyClass a;
a.abc^
}
''';
// Tell the server we only support the Field CompletionItemKind.
await initialize(
textDocumentCapabilities: withCompletionItemKinds(
emptyTextDocumentClientCapabilities, [CompletionItemKind.Field]),
);
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final kinds = res.map((item) => item.kind).toList();
// Ensure we only get nulls or Fields (the sample code contains Classes).
expect(
kinds,
everyElement(anyOf(isNull, equals(CompletionItemKind.Field))),
);
}
test_completionTriggerKinds_invalidParams() async {
await initialize();
final invalidTriggerKind = CompletionTriggerKind.fromJson(-1);
final request = getCompletion(
mainFileUri,
new Position(0, 0),
context: new CompletionContext(invalidTriggerKind, 'A'),
);
await expectLater(
request, throwsA(isResponseError(ErrorCodes.InvalidParams)));
}
test_gettersAndSetters() async {
final content = '''
class MyClass {
String get justGetter => '';
String set justSetter(String value) {}
String get getterAndSetter => '';
String set getterAndSetter(String value) {}
}
main() {
MyClass a;
a.^
}
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final getter = res.singleWhere((c) => c.label == 'justGetter');
final setter = res.singleWhere((c) => c.label == 'justSetter');
final both = res.singleWhere((c) => c.label == 'getterAndSetter');
expect(getter.detail, equals('String'));
expect(setter.detail, equals('String'));
expect(both.detail, equals('String'));
[getter, setter, both].forEach((item) {
expect(item.kind, equals(CompletionItemKind.Property));
});
}
test_insideString() async {
final content = '''
var a = "This is ^a test"
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
expect(res, isEmpty);
}
test_isDeprecated_notSupported() async {
final content = '''
class MyClass {
@deprecated
String abcdefghij;
}
main() {
MyClass a;
a.abc^
}
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final item = res.singleWhere((c) => c.label == 'abcdefghij');
expect(item.deprecated, isNull);
// If the does not say it supports the deprecated flag, we should show
// '(deprecated)' in the details.
expect(item.detail.toLowerCase(), contains('deprecated'));
}
test_isDeprecated_supported() async {
final content = '''
class MyClass {
@deprecated
String abcdefghij;
}
main() {
MyClass a;
a.abc^
}
''';
await initialize(
textDocumentCapabilities: withCompletionItemDeprecatedSupport(
emptyTextDocumentClientCapabilities));
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
final item = res.singleWhere((c) => c.label == 'abcdefghij');
expect(item.deprecated, isTrue);
// If the client says it supports the deprecated flag, we should not show
// deprecated in the details.
expect(item.detail, isNot(contains('deprecated')));
}
test_nonDartFile() async {
newFile(pubspecFilePath, content: simplePubspecContent);
await initialize();
final res = await getCompletion(pubspecFileUri, startOfDocPos);
expect(res, isEmpty);
}
test_plainText() async {
final content = '''
class MyClass {
String abcdefghij;
}
main() {
MyClass a;
a.abc^
}
''';
await initialize();
await openFile(mainFileUri, withoutMarkers(content));
final res = await getCompletion(mainFileUri, positionFromMarker(content));
expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
final item = res.singleWhere((c) => c.label == 'abcdefghij');
expect(item.insertTextFormat, equals(InsertTextFormat.PlainText));
// ignore: deprecated_member_use_from_same_package
expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
expect(updated, contains('a.abcdefghij'));
}
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
}
'''));
}
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 {
String abcdefghij;
}
main() {
MyClass a;
a.abc^
}
''';
newFile(mainFilePath, content: withoutMarkers(content));
await initialize();
final res = await getCompletion(mainFileUri, positionFromMarker(content));
expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
final item = res.singleWhere((c) => c.label == 'abcdefghij');
expect(item.insertTextFormat, equals(InsertTextFormat.PlainText));
// ignore: deprecated_member_use_from_same_package
expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
expect(updated, contains('a.abcdefghij'));
}
}