| // 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 'dart:async'; |
| |
| import 'package:analysis_server/lsp_protocol/protocol.dart'; |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/lsp/constants.dart'; |
| import 'package:analysis_server/src/lsp/handlers/handler_completion.dart'; |
| import 'package:analysis_server/src/services/linter/lint_names.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/class_declaration.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/do_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/flutter_stateful_widget.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/flutter_stateful_widget_with_animation.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/flutter_stateless_widget.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/for_in_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/for_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/function_declaration.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/if_else_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/if_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/main_function.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/switch_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/test_definition.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/test_group_definition.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/try_catch_statement.dart'; |
| import 'package:analysis_server/src/services/snippets/dart/while_statement.dart'; |
| import 'package:analyzer/src/test_utilities/test_code_format.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; |
| import 'package:collection/collection.dart'; |
| import 'package:linter/src/rules.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../tool/lsp_spec/matchers.dart'; |
| import '../utils/test_code_extensions.dart'; |
| import 'completion.dart'; |
| import 'server_abstract.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(CompletionTest); |
| defineReflectiveTests(CompletionLabelDetailsTest); |
| defineReflectiveTests(CompletionDocumentationResolutionTest); |
| defineReflectiveTests(DartSnippetCompletionTest); |
| defineReflectiveTests(FlutterSnippetCompletionTest); |
| }); |
| } |
| |
| abstract class AbstractCompletionTest extends AbstractLspAnalysisServerTest |
| with CompletionTestMixin { |
| AbstractCompletionTest() { |
| defaultInitializationOptions = { |
| // Default to a high budget for tests because everything is cold and |
| // may take longer to return. |
| 'completionBudgetMilliseconds': 50000 |
| }; |
| } |
| |
| void expectDocumentation(CompletionItem completion, Matcher matcher) { |
| var docs = completion.documentation?.map( |
| (markup) => markup.value, |
| (string) => string, |
| ); |
| expect(docs, matcher); |
| } |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| |
| // Completion tests have incomplete code. |
| failTestOnErrorDiagnostic = false; |
| |
| setApplyEditSupport(); |
| } |
| } |
| |
| @reflectiveTest |
| class CompletionDocumentationResolutionTest extends AbstractCompletionTest { |
| late String content; |
| late final code = TestCode.parse(content); |
| |
| Future<CompletionItem> getCompletionItem(String label) async { |
| var completions = await getCompletion(mainFileUri, code.position.position); |
| return completions.singleWhere((c) => c.label == label); |
| } |
| |
| Future<void> initializeServer() async { |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| } |
| |
| Future<void> test_class() async { |
| newFile( |
| join(projectFolderPath, 'my_class.dart'), |
| ''' |
| /// Class. |
| class MyClass {} |
| ''', |
| ); |
| |
| content = ''' |
| void f() { |
| MyClass^ |
| } |
| '''; |
| |
| await initializeServer(); |
| |
| var completion = await getCompletionItem('MyClass'); |
| expectDocumentation(completion, isNull); |
| |
| var resolved = await resolveCompletion(completion); |
| expectDocumentation(resolved, contains('Class.')); |
| } |
| |
| Future<void> test_class_constructor() async { |
| newFile( |
| join(projectFolderPath, 'my_class.dart'), |
| ''' |
| class MyClass { |
| /// Constructor. |
| MyClass(); |
| } |
| ''', |
| ); |
| |
| content = ''' |
| void f() { |
| MyClass^ |
| } |
| '''; |
| |
| await initializeServer(); |
| |
| var completion = await getCompletionItem('MyClass()'); |
| expectDocumentation(completion, isNull); |
| |
| var resolved = await resolveCompletion(completion); |
| expectDocumentation(resolved, contains('Constructor.')); |
| } |
| |
| Future<void> test_class_constructorNamed() async { |
| newFile( |
| join(projectFolderPath, 'my_class.dart'), |
| ''' |
| class MyClass { |
| /// Named Constructor. |
| MyClass.named(); |
| } |
| ''', |
| ); |
| |
| content = ''' |
| void f() { |
| MyClass^ |
| } |
| '''; |
| |
| await initializeServer(); |
| |
| var completion = await getCompletionItem('MyClass.named()'); |
| expectDocumentation(completion, isNull); |
| |
| var resolved = await resolveCompletion(completion); |
| expectDocumentation(resolved, contains('Named Constructor.')); |
| } |
| |
| Future<void> test_enum() async { |
| newFile( |
| join(projectFolderPath, 'my_enum.dart'), |
| ''' |
| /// Enum. |
| enum MyEnum {} |
| ''', |
| ); |
| |
| content = ''' |
| void f() { |
| MyEnum^ |
| } |
| '''; |
| |
| await initializeServer(); |
| |
| var completion = await getCompletionItem('MyEnum'); |
| expectDocumentation(completion, isNull); |
| |
| var resolved = await resolveCompletion(completion); |
| expectDocumentation(resolved, contains('Enum.')); |
| } |
| |
| Future<void> test_enum_member() async { |
| // Function used to provide type context in main file without importing |
| // the enum. |
| newFile( |
| join(projectFolderPath, 'lib', 'func.dart'), |
| ''' |
| import 'my_enum.dart'; |
| void enumFunc(MyEnum e) {} |
| ''', |
| ); |
| |
| newFile( |
| join(projectFolderPath, 'lib', 'my_enum.dart'), |
| ''' |
| enum MyEnum { |
| /// Enum Member. |
| one, |
| } |
| ''', |
| ); |
| |
| content = ''' |
| import 'func.dart'; |
| void f() { |
| enumFunc(MyEnum^) |
| } |
| '''; |
| |
| await initializeServer(); |
| |
| var completion = await getCompletionItem('MyEnum.one'); |
| expectDocumentation(completion, isNull); |
| |
| var resolved = await resolveCompletion(completion); |
| expectDocumentation(resolved, contains('Enum Member.')); |
| } |
| } |
| |
| @reflectiveTest |
| class CompletionLabelDetailsTest extends AbstractCompletionTest { |
| late String fileAPath; |
| |
| Future<void> expectLabels( |
| String content, { |
| Uri? completionFileUri, |
| // Main label of the completion (eg 'myFunc') |
| required String? label, |
| // The detail part of the label (shown after label, usually truncated signature) |
| required String? labelDetail, |
| // Additional label description (usually the auto-import URI) |
| required String? labelDescription, |
| // Filter text (usually same as label, never with `()` or other suffixes) |
| required String? filterText, |
| // Main detail (shown in popout, usually full signature) |
| required String? detail, |
| // Sometimes resolved detail has a prefix added (eg. "Auto-import from"). |
| String? resolvedDetailPrefix, |
| }) async { |
| completionFileUri ??= mainFileUri; |
| |
| var code = TestCode.parse(content); |
| await initialize(); |
| await openFile(completionFileUri, code.code); |
| |
| var completions = |
| await getCompletion(completionFileUri, code.position.position); |
| var completion = completions.singleWhereOrNull((c) => c.label == label); |
| if (completion == null) { |
| fail('Did not find completion "$label" in completion results:' |
| '\n ${completions.map((c) => c.label).join('\n ')}'); |
| } |
| |
| expect(completion.detail, detail); |
| expect(completion.filterText, filterText); |
| |
| // If both fields are expected to be null, expect the whole object to be |
| // null (to reduce payload size). |
| if (labelDetail == null && labelDescription == null) { |
| expect(completion.labelDetails, isNull); |
| } else { |
| var labelDetails = completion.labelDetails; |
| if (labelDetails == null) { |
| fail('Completion "$label" does not have labelDetails'); |
| } |
| expect(labelDetails.detail, labelDetail); |
| expect(labelDetails.description, labelDescription); |
| } |
| |
| // Verify that resolution does not modify these results. |
| var resolved = await resolveCompletion(completion); |
| expect(resolved.label, completion.label); |
| expect(resolved.filterText, completion.filterText); |
| expect( |
| resolved.detail, |
| resolvedDetailPrefix != null |
| ? '$resolvedDetailPrefix${completion.detail ?? ''}' |
| : completion.detail, |
| ); |
| expect(resolved.labelDetails?.detail, completion.labelDetails?.detail); |
| expect( |
| resolved.labelDetails?.description, |
| completion.labelDetails?.description, |
| ); |
| } |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| fileAPath = join(projectFolderPath, 'lib', 'a.dart'); |
| |
| // TODO(dantup): Consider enabling this by default for [CompletionTest] and |
| // changing this class to test support without it (or, subclassing |
| // CompletionTest and inferring the label when labelDetails are not |
| // supported). |
| setCompletionItemLabelDetailsSupport(); |
| } |
| |
| Future<void> test_constructor_argument() async { |
| var content = ''' |
| var a = Foo(^); |
| |
| class Foo { |
| final int value; |
| const Foo({required this.value}); |
| } |
| '''; |
| |
| await expectLabels( |
| content, |
| label: 'value:', |
| labelDetail: ' int', |
| labelDescription: null, |
| filterText: null, |
| detail: 'int', |
| ); |
| } |
| |
| Future<void> test_constructor_factory_argument() async { |
| var content = ''' |
| var a = Foo(^); |
| |
| class Foo { |
| final int value; |
| const Foo._({required this.value}); |
| const factory Foo({required int value}) = Foo._; |
| } |
| '''; |
| |
| await expectLabels( |
| content, |
| label: 'value:', |
| labelDetail: ' int', |
| labelDescription: null, |
| filterText: null, |
| detail: 'int', |
| ); |
| } |
| |
| Future<void> test_imported_function_returnType_args() async { |
| newFile(fileAPath, ''' |
| String a(String a, {String b}) {} |
| '''); |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '(…) → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a, {String b}) → String'); |
| } |
| |
| Future<void> test_imported_function_returnType_noArgs() async { |
| newFile(fileAPath, ''' |
| String a() {} |
| '''); |
| var content = ''' |
| import 'a.dart'; |
| String f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '() → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '() → String'); |
| } |
| |
| Future<void> test_imported_function_void_args() async { |
| newFile(fileAPath, ''' |
| void a(String a, {String b}) {} |
| '''); |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '(…) → void', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a, {String b}) → void'); |
| } |
| |
| Future<void> test_imported_function_void_noArgs() async { |
| newFile(fileAPath, ''' |
| void a() {} |
| '''); |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '() → void', |
| labelDescription: null, |
| filterText: null, |
| detail: '() → void'); |
| } |
| |
| Future<void> test_local_function_returnType_args() async { |
| var content = ''' |
| String f(String a, {String b}) { |
| f^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'f', |
| labelDetail: '(…) → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a, {String b}) → String'); |
| } |
| |
| Future<void> test_local_function_returnType_noArgs() async { |
| var content = ''' |
| String f() { |
| f^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'f', |
| labelDetail: '() → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '() → String'); |
| } |
| |
| Future<void> test_local_function_void_args() async { |
| var content = ''' |
| void f(String a, {String b}) { |
| f^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'f', |
| labelDetail: '(…) → void', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a, {String b}) → void'); |
| } |
| |
| Future<void> test_local_function_void_noArgs() async { |
| var content = ''' |
| void f() { |
| f^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'f', |
| labelDetail: '() → void', |
| labelDescription: null, |
| filterText: null, |
| detail: '() → void'); |
| } |
| |
| Future<void> test_local_getter() async { |
| var content = ''' |
| String a => ''; |
| void f() { |
| a^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '() → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '() → String'); |
| } |
| |
| Future<void> test_local_getterAndSetter() async { |
| var content = ''' |
| set a(String value) {} |
| String get a => ''; |
| void f() { |
| a^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: ' String', |
| labelDescription: null, |
| filterText: null, |
| detail: 'String'); |
| } |
| |
| Future<void> test_local_override_annotation_equals() async { |
| var content = ''' |
| class Base { |
| } |
| |
| class Derived extends Base { |
| @over^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'override ==', |
| labelDetail: '(…) → bool', |
| labelDescription: null, |
| filterText: null, |
| detail: '(Object other) → bool'); |
| } |
| |
| Future<void> test_local_override_annotation_method() async { |
| var content = ''' |
| class Base { |
| String aa(String a) => ''; |
| } |
| |
| class Derived extends Base { |
| @over^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'override aa', |
| labelDetail: '(…) → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a) → String'); |
| } |
| |
| Future<void> test_local_override_name() async { |
| var content = ''' |
| class Base { |
| String aa(String a) => ''; |
| } |
| |
| class Derived extends Base { |
| a^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'aa', |
| labelDetail: '(…) → String', |
| labelDescription: null, |
| filterText: null, |
| detail: '(String a) → String'); |
| } |
| |
| Future<void> test_local_setter() async { |
| var content = ''' |
| set a(String value) {} |
| void f() { |
| a^ |
| } |
| '''; |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: ' String', |
| labelDescription: null, |
| filterText: null, |
| detail: 'String'); |
| } |
| |
| Future<void> test_notImported_function_returnType_args() async { |
| newFile(fileAPath, ''' |
| String a(String a, {String b}) {} |
| '''); |
| var content = ''' |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '(…) → String', |
| labelDescription: 'package:test/a.dart', |
| filterText: null, |
| detail: '(String a, {String b}) → String', |
| resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n"); |
| } |
| |
| Future<void> test_notImported_function_returnType_noArgs() async { |
| newFile(fileAPath, ''' |
| String a() {} |
| '''); |
| var content = ''' |
| String f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '() → String', |
| labelDescription: 'package:test/a.dart', |
| filterText: null, |
| detail: '() → String', |
| resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n"); |
| } |
| |
| Future<void> test_notImported_function_void_args() async { |
| newFile(fileAPath, ''' |
| void a(String a, {String b}) {} |
| '''); |
| var content = ''' |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '(…) → void', |
| labelDescription: 'package:test/a.dart', |
| filterText: null, |
| detail: '(String a, {String b}) → void', |
| resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n"); |
| } |
| |
| Future<void> test_notImported_function_void_noArgs() async { |
| newFile(fileAPath, ''' |
| void a() {} |
| '''); |
| var content = ''' |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'a', |
| labelDetail: '() → void', |
| labelDescription: 'package:test/a.dart', |
| filterText: null, |
| detail: '() → void', |
| resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n"); |
| } |
| |
| Future<void> test_notImported_outsideLib_relativePath() async { |
| var testMainFilePath = join(projectFolderPath, 'test', 'main.dart'); |
| var testFileAPath = join(projectFolderPath, 'test', 'a.dart'); |
| |
| newFile(testFileAPath, ''' |
| void a(String a, {String b}) {} |
| '''); |
| var content = ''' |
| void f() { |
| a^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| completionFileUri: toUri(testMainFilePath), |
| label: 'a', |
| labelDetail: '(…) → void', |
| labelDescription: 'a.dart', |
| filterText: null, |
| detail: '(String a, {String b}) → void', |
| resolvedDetailPrefix: "Auto import from 'a.dart'\n\n"); |
| } |
| |
| Future<void> test_nullNotEmpty() async { |
| var content = ''' |
| bool a = ^ |
| '''; |
| |
| /// expectLabels verifies the whole labelDetails object is null if |
| /// both fields are expected to be null. |
| await expectLabels( |
| content, |
| label: 'true', |
| labelDetail: null, |
| labelDescription: null, |
| filterText: null, |
| detail: null, |
| ); |
| } |
| |
| Future<void> test_record() async { |
| var content = r''' |
| void f((int, int) record) { |
| record.$^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: r'$1', |
| labelDetail: ' int', |
| labelDescription: null, |
| filterText: null, |
| detail: 'int'); |
| } |
| |
| Future<void> test_variable() async { |
| var content = r''' |
| void f(int variable) { |
| varia^ |
| } |
| '''; |
| |
| await expectLabels(content, |
| label: 'variable', |
| labelDetail: ' int', |
| labelDescription: null, |
| filterText: null, |
| detail: 'int'); |
| } |
| } |
| |
| @reflectiveTest |
| class CompletionTest extends AbstractCompletionTest { |
| /// Checks whether the correct types of documentation are returned for |
| /// completions based on [preference]. |
| Future<void> assertDocumentation( |
| String? preference, { |
| required bool includesSummary, |
| required bool includesFull, |
| }) async { |
| var content = ''' |
| /// Summary. |
| /// |
| /// Full. |
| class A {} |
| |
| A^ |
| '''; |
| |
| await provideConfig( |
| initialize, |
| { |
| if (preference != null) 'documentation': preference, |
| }, |
| ); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var completion = res.singleWhere((c) => c.label == 'A'); |
| |
| if (includesSummary) { |
| expectDocumentation(completion, contains('Summary.')); |
| } else { |
| expectDocumentation(completion, isNot(contains('Summary.'))); |
| } |
| |
| if (includesFull) { |
| expectDocumentation(completion, contains('Full.')); |
| } else { |
| expectDocumentation(completion, isNot(contains('Full.'))); |
| } |
| } |
| |
| /// Checks whether the correct types of documentation are returned during |
| /// `completionItem/resolve` based on [preference]. |
| Future<void> assertResolvedDocumentation( |
| String? preference, { |
| required bool includesSummary, |
| required bool includesFull, |
| }) async { |
| newFile( |
| join(projectFolderPath, 'other_file.dart'), |
| ''' |
| /// Summary. |
| /// |
| /// Full. |
| class InOtherFile {} |
| ''', |
| ); |
| |
| var content = ''' |
| void f() { |
| InOtherF^ |
| } |
| '''; |
| |
| await provideConfig( |
| initialize, |
| { |
| if (preference != null) 'documentation': preference, |
| }, |
| ); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var completion = res.singleWhere((c) => c.label == 'InOtherFile'); |
| |
| // Expect no docs in original response and correct type of docs added |
| // during resolve. |
| expectDocumentation(completion, isNull); |
| var resolved = await resolveCompletion(completion); |
| |
| if (includesSummary) { |
| expectDocumentation(resolved, contains('Summary.')); |
| } else { |
| expectDocumentation(resolved, isNot(contains('Summary.'))); |
| } |
| |
| if (includesFull) { |
| expectDocumentation(resolved, contains('Full.')); |
| } else { |
| expectDocumentation(resolved, isNot(contains('Full.'))); |
| } |
| } |
| |
| Future<void> checkCompleteFunctionCallInsertText( |
| String content, |
| String completion, { |
| required String? editText, |
| InsertTextFormat? insertTextFormat, |
| }) async { |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere( |
| (c) => c.label == completion, |
| orElse: () => |
| throw 'Did not find $completion in ${res.map((r) => r.label).toList()}', |
| ); |
| |
| expect(item.insertTextFormat, equals(insertTextFormat)); |
| // We always expect `insertText` to be `null` now, as we always use |
| // `textEdit`. |
| expect(item.insertText, isNull); |
| |
| // And the expected text should be in the `textEdit`. |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, equals(editText)); |
| expect(textEdit.range, equals(code.range.range)); |
| } |
| |
| void expectAutoImportCompletion(List<CompletionItem> items, String file) { |
| expect( |
| items.singleWhereOrNull( |
| (c) => c.detail?.contains("Auto import from '$file'") ?? false), |
| isNotNull, |
| ); |
| } |
| |
| /// Expect [item] to use the default edit range, inserting the value [text]. |
| void expectUsesDefaultEditRange(CompletionItem item, String text) { |
| expect(item.textEditText ?? item.label, text); |
| expect(item.insertText, isNull); |
| expect(item.textEdit, isNull); |
| } |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| writeTestPackageConfig( |
| flutter: true, |
| ); |
| } |
| |
| Future<void> test_annotation_beforeMember() async { |
| var content = ''' |
| class B { |
| @^ |
| int a = 1; |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var completions = await getCompletion(mainFileUri, code.position.position); |
| var labels = completions.map((c) => c.label).toList(); |
| expect(labels, contains('override')); |
| expect(labels, contains('deprecated')); |
| expect(labels, contains('Deprecated(…)')); |
| } |
| |
| Future<void> test_annotation_endOfClass() async { |
| var content = ''' |
| class B { |
| @^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var completions = await getCompletion(mainFileUri, code.position.position); |
| var labels = completions.map((c) => c.label).toList(); |
| expect(labels, contains('override')); |
| expect(labels, contains('deprecated')); |
| expect(labels, contains('Deprecated(…)')); |
| } |
| |
| Future<void> test_closure() async { |
| var content = ''' |
| void f({void Function(int a, String b) closure}) {} |
| |
| void g() { |
| f(closure: ^); |
| } |
| '''; |
| |
| var expectedContent = ''' |
| void f({void Function(int a, String b) closure}) {} |
| |
| void g() { |
| f(closure: (a, b) => ^,); |
| } |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectCompletions: ['(a, b) {}', '(a, b) =>'], |
| applyEditsFor: '(a, b) =>', |
| expectedContent: expectedContent, |
| ); |
| } |
| |
| Future<void> test_closure_requiredNamed() async { |
| var content = ''' |
| void f({void Function({int a, required String b}) closure}) {} |
| |
| void g() { |
| f(closure: ^); |
| } |
| '''; |
| |
| var expectedContent = ''' |
| void f({void Function({int a, required String b}) closure}) {} |
| |
| void g() { |
| f(closure: ({a, required b}) => ^,); |
| } |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| // Display text does not contain 'required' because it makes the |
| // completion much longer, we just include it in the completion text. |
| expectCompletions: ['({a, b}) {}', '({a, b}) =>'], |
| applyEditsFor: '({a, b}) =>', |
| expectedContent: expectedContent, |
| ); |
| } |
| |
| Future<void> test_comment() async { |
| var content = ''' |
| // foo ^ |
| void f() {} |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res, isEmpty); |
| } |
| |
| Future<void> test_comment_endOfFile_withNewline() async { |
| // Checks for a previous bug where invoking completion inside a comment |
| // at the end of a file would return results. |
| var content = ''' |
| // foo ^ |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res, isEmpty); |
| } |
| |
| Future<void> test_comment_endOfFile_withoutNewline() async { |
| // Checks for a previous bug where invoking completion inside a comment |
| // at the very end of a file with no trailing newline would return results. |
| var content = '// foo ^'; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res, isEmpty); |
| } |
| |
| Future<void> test_commitCharacter_dynamicRegistration() async { |
| var registrations = <Registration>[]; |
| // Provide empty config and collect dynamic registrations during |
| // initialization. |
| setDidChangeConfigurationDynamicRegistration(); |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| await monitorDynamicRegistrations( |
| registrations, |
| () => provideConfig(initialize, {}), |
| ); |
| |
| Registration registration(Method method) => |
| registrationForDart(registrations, method); |
| |
| // By default, there should be no commit characters. |
| var reg = registration(Method.textDocument_completion); |
| var options = CompletionRegistrationOptions.fromJson( |
| reg.registerOptions as Map<String, Object?>); |
| expect(options.allCommitCharacters, isNull); |
| |
| // When we change config, we should get a re-registration (unregister then |
| // register) for completion which now includes the commit characters. |
| await monitorDynamicReregistration( |
| registrations, |
| () => updateConfig({'previewCommitCharacters': true}), |
| ); |
| reg = registration(Method.textDocument_completion); |
| options = CompletionRegistrationOptions.fromJson( |
| reg.registerOptions as Map<String, Object?>); |
| expect(options.allCommitCharacters, equals(dartCompletionCommitCharacters)); |
| } |
| |
| Future<void> test_completeFunctionCalls_constructor() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| Aaaaa(int a); |
| } |
| void f(int aaa) { |
| var a = new [!Aaa^!] |
| } |
| ''', |
| 'Aaaaa(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| editText: r'Aaaaa(${0:a})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_escapesDollarArgs() => |
| checkCompleteFunctionCallInsertText( |
| r''' |
| int myFunction(String a$a, int b, {String c}) { |
| var a = [!myFu^!] |
| } |
| ''', |
| 'myFunction(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| // The dollar should have been escaped. |
| editText: r'myFunction(${1:a\$a}, ${2:b})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_escapesDollarName() => |
| checkCompleteFunctionCallInsertText( |
| r''' |
| int myFunc$tion(String a, int b, {String c}) { |
| var a = [!myFu^!] |
| } |
| ''', |
| r'myFunc$tion(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| // The dollar should have been escaped. |
| editText: r'myFunc\$tion(${1:a}, ${2:b})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_constructor() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| Aaaaa(int a); |
| } |
| void f(int aaa) { |
| var a = new [!Aaa^!]() |
| } |
| ''', |
| 'Aaaaa(…)', |
| editText: 'Aaaaa', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_expression() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| int myFunction(String a, int b, {String c}) { |
| var a = [!myFu^!]() |
| } |
| ''', |
| 'myFunction(…)', |
| editText: 'myFunction', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_member_noPrefix() => |
| // https://github.com/Dart-Code/Dart-Code/issues/3672 |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| static foo(int a) {} |
| } |
| void f() { |
| Aaaaa.[!^!]() |
| } |
| ''', |
| 'foo(…)', |
| editText: 'foo', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_namedConstructor() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| Aaaaa.foo(int a); |
| } |
| void f() { |
| var a = new Aaaaa.[!foo^!]() |
| } |
| ''', |
| 'foo(…)', |
| editText: 'foo', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_statement() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| void f(int a) { |
| [!f^!]() |
| } |
| ''', |
| 'f(…)', |
| editText: 'f', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingArgList_suggestionSets() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| void f(int a) { |
| [!pri^!]() |
| } |
| ''', |
| 'print(…)', |
| editText: 'print', |
| ); |
| |
| Future<void> test_completeFunctionCalls_existingPartialArgList() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| Aaaaa(int a); |
| } |
| void f(int aaa) { |
| var a = new [!Aaa^!]( |
| } |
| ''', |
| 'Aaaaa(…)', |
| editText: 'Aaaaa', |
| ); |
| |
| Future<void> test_completeFunctionCalls_expression() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| int myFunction(String a, int b, {String c}) { |
| var a = [!myFu^!] |
| } |
| ''', |
| 'myFunction(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| editText: r'myFunction(${1:a}, ${2:b})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_flutterSetState() async { |
| // Flutter's setState method has special handling inside SuggestionBuilder |
| // that already adds in a selection (which overlaps with completeFunctionCalls). |
| // Ensure we don't end up with two sets of parens/placeholders in this case. |
| var content = ''' |
| import 'package:flutter/material.dart'; |
| |
| class MyWidget extends StatefulWidget { |
| @override |
| State<MyWidget> createState() => _MyWidgetState(); |
| } |
| |
| class _MyWidgetState extends State<MyWidget> { |
| @override |
| Widget build(BuildContext context) { |
| [!setSt^!] |
| return const Placeholder(); |
| } |
| } |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label.startsWith('setState(')); |
| |
| // Usually the label would be "setState(…)" but here it's slightly different |
| // to indicate a full statement is being inserted. |
| expect(item.label, equals('setState(() {});')); |
| |
| // Ensure the snippet comes through in the expected format with the expected |
| // placeholders. |
| expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, 'setState(() {\n \$0\n });'); |
| expect(textEdit.range, equals(code.range.range)); |
| } |
| |
| Future<void> test_completeFunctionCalls_namedConstructor() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| class Aaaaa { |
| Aaaaa.foo(int a); |
| } |
| void f() { |
| var a = new Aaaaa.[!foo^!] |
| } |
| ''', |
| 'foo(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| editText: r'foo(${0:a})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_noParameters() async { |
| var content = ''' |
| void myFunction() {} |
| |
| void f() { |
| [!myFu^!] |
| } |
| '''; |
| |
| await checkCompleteFunctionCallInsertText( |
| content, |
| 'myFunction()', |
| editText: 'myFunction()', |
| insertTextFormat: InsertTextFormat.Snippet, |
| ); |
| } |
| |
| Future<void> test_completeFunctionCalls_optionalParameters() async { |
| var content = ''' |
| void myFunction({int a}) {} |
| |
| void f() { |
| [!myFu^!] |
| } |
| '''; |
| |
| await checkCompleteFunctionCallInsertText( |
| content, |
| 'myFunction(…)', |
| // With optional params, there should still be parens/tab stop inside. |
| editText: r'myFunction($0)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| ); |
| } |
| |
| Future<void> test_completeFunctionCalls_requiredNamed() async { |
| var content = ''' |
| void myFunction(String a, int b, {required String c, String d = ''}) {} |
| |
| void f() { |
| [!myFu^!] |
| } |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'myFunction(…)'); |
| // Ensure the snippet comes through in the expected format with the expected |
| // placeholders. |
| expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})'); |
| expect(textEdit.range, equals(code.range.range)); |
| } |
| |
| Future<void> test_completeFunctionCalls_requiredNamed_suggestionSet() async { |
| var otherFile = join(projectFolderPath, 'lib', 'other.dart'); |
| newFile( |
| otherFile, |
| "void myFunction(String a, int b, {required String c, String d = ''}) {}", |
| ); |
| var content = ''' |
| void f() { |
| [!myFu^!] |
| } |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'myFunction(…)'); |
| // Ensure the snippet comes through in the expected format with the expected |
| // placeholders. |
| expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| expect(item.insertText, isNull); |
| expect(item.textEdit, isNotNull); |
| var originalTextEdit = item.textEdit; |
| |
| // Ensure the item can be resolved and retains the correct textEdit (since |
| // textEdit may be recomputed during resolve). |
| var resolved = await resolveCompletion(item); |
| expect(resolved.insertText, isNull); |
| expect(resolved.textEdit, originalTextEdit); |
| var textEdit = toTextEdit(resolved.textEdit!); |
| expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})'); |
| expect(textEdit.range, equals(code.range.range)); |
| } |
| |
| Future<void> |
| test_completeFunctionCalls_resolve_producesCorrectEditWithoutInsertText() async { |
| // Ensure our `resolve` call does not rely on the presence of `insertText` |
| // to compute the correct edits. This is something we did incorrectly in the |
| // past and broke with |
| // https://github.com/dart-lang/sdk/commit/40e25ebad0bd008615b1c1d8021cb27839f00dcd |
| // because the way these are combined in the VS Code LSP client means we are |
| // not provided both `insertText` and `textEdit` back in the resolve call. |
| // |
| // Now, we never supply `insertText` and always use `textEdit`. |
| var content = ''' |
| final a = Stri^ |
| '''; |
| |
| /// Helper to verify a completion is as expected. |
| void expectCorrectCompletion(CompletionItem item) { |
| // Ensure this completion looks as we'd expect. |
| expect(item.label, 'String.fromCharCode(…)'); |
| expect(item.insertText, isNull); |
| expect( |
| item.textEdit!.map((edit) => edit.newText, (edit) => edit.newText), |
| r'String.fromCharCode(${0:charCode})', |
| ); |
| } |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var completion = |
| res.singleWhere((c) => c.label == 'String.fromCharCode(…)'); |
| expectCorrectCompletion(completion); |
| |
| var resolved = await resolveCompletion(completion); |
| expectCorrectCompletion(resolved); |
| } |
| |
| Future<void> test_completeFunctionCalls_show() async { |
| var content = ''' |
| import 'dart:math' show mi^ |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'completeFunctionCalls': true}); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'min(…)'); |
| // The insert text should be a simple string with no parens/args and |
| // no need for snippets. |
| expect(item.insertTextFormat, isNull); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, r'min'); |
| } |
| |
| Future<void> test_completeFunctionCalls_statement() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| void f(int a) { |
| [!f^!] |
| } |
| ''', |
| 'f(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| editText: r'f(${0:a})', |
| ); |
| |
| Future<void> test_completeFunctionCalls_suggestionSets() => |
| checkCompleteFunctionCallInsertText( |
| ''' |
| void f(int a) { |
| [!pri^!] |
| } |
| ''', |
| 'print(…)', |
| insertTextFormat: InsertTextFormat.Snippet, |
| editText: r'print(${0:object})', |
| ); |
| |
| Future<void> test_completionKinds_default() async { |
| newFile(join(projectFolderPath, 'file.dart'), ''); |
| newFolder(join(projectFolderPath, 'folder')); |
| |
| var content = "import '^';"; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var file = res.singleWhere((c) => c.label == 'file.dart'); |
| var folder = res.singleWhere((c) => c.label == 'folder/'); |
| var 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)); |
| } |
| |
| Future<void> test_completionKinds_imports() async { |
| // Tell the server we support some specific CompletionItemKinds. |
| setCompletionItemKinds([ |
| CompletionItemKind.File, |
| CompletionItemKind.Folder, |
| CompletionItemKind.Module, |
| ]); |
| |
| var content = "import '^';"; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var file = res.singleWhere((c) => c.label == 'file.dart'); |
| var folder = res.singleWhere((c) => c.label == 'folder/'); |
| var 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)); |
| } |
| |
| Future<void> test_completionKinds_supportedSubset() async { |
| // Tell the server we only support the Field CompletionItemKind. |
| setCompletionItemKinds([CompletionItemKind.Field]); |
| |
| var content = ''' |
| class MyClass { |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var 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))), |
| ); |
| } |
| |
| Future<void> test_completionTrigger_brace_block() async { |
| // Brace should not trigger completion if a normal code block. |
| var content = r''' |
| main () {^} |
| '''; |
| await _checkResultsForTriggerCharacters(content, ['{'], isEmpty); |
| } |
| |
| Future<void> |
| test_completionTrigger_brace_interpolatedStringExpression() async { |
| // Brace should trigger completion if at the start of an interpolated expression |
| var content = r''' |
| var a = '${^'; |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r'{'], isNotEmpty); |
| } |
| |
| Future<void> test_completionTrigger_brace_rawString() async { |
| // Brace should not trigger completion if in a raw string. |
| var content = r''' |
| var a = r'${^'; |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_brace_string() async { |
| // Brace should not trigger completion if not at the start of an interpolated |
| // expression. |
| var content = r''' |
| var a = '{^'; |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_colon_argument() async { |
| // Colons should trigger completion after argument names. |
| var content = r''' |
| void f({int? a}) { |
| f(a:^ |
| } |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r':'], isNotEmpty); |
| } |
| |
| Future<void> test_completionTrigger_colon_case() async { |
| // Colons should not trigger completion in a switch case. |
| var content = r''' |
| void f(int a) { |
| switch (a) { |
| case:^ |
| } |
| } |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r':'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_colon_default() async { |
| // Colons should not trigger completion in a switch case. |
| var content = r''' |
| void f(int a) { |
| switch (a) { |
| default:^ |
| } |
| } |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r':'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_colon_import() async { |
| // Colons should trigger completion after argument names. |
| var content = r''' |
| import 'package:^'; |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r':'], isNotEmpty); |
| } |
| |
| Future<void> test_completionTrigger_quotes_endingString() async { |
| // Completion triggered by a quote ending a string should not return results. |
| var content = "foo(''^);"; |
| await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_quotes_startingImport() async { |
| // Completion triggered by a quote for import should return results. |
| var content = "import '^'"; |
| await _checkResultsForTriggerCharacters(content, ["'", '"'], isNotEmpty); |
| } |
| |
| Future<void> test_completionTrigger_quotes_startingString() async { |
| // Completion triggered by a quote for normal string should not return results. |
| var content = "foo('^');"; |
| await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_quotes_terminatingImport() async { |
| // Completion triggered by a quote ending an import should not return results. |
| var content = "import ''^"; |
| await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); |
| } |
| |
| Future<void> test_completionTrigger_slash_directivePath() async { |
| // Slashes should trigger completion when typing in directive paths, eg. |
| // after typing 'package:foo/' completion should give the next folder segments. |
| var content = r''' |
| import 'package:test/^'; |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r'/'], isNotEmpty); |
| } |
| |
| Future<void> test_completionTrigger_slash_divide() async { |
| // Slashes should not trigger completion when typing in a normal expression. |
| var content = r''' |
| var a = 1 /^ |
| '''; |
| await _checkResultsForTriggerCharacters(content, [r'/'], isEmpty); |
| } |
| |
| Future<void> test_completionTriggerKinds_invalidParams() async { |
| await initialize(); |
| |
| var invalidTriggerKind = CompletionTriggerKind.fromJson(-1); |
| var request = getCompletion( |
| mainFileUri, |
| Position(line: 0, character: 0), |
| context: CompletionContext( |
| triggerKind: invalidTriggerKind, triggerCharacter: 'A'), |
| ); |
| |
| await expectLater( |
| request, throwsA(isResponseError(ErrorCodes.InvalidParams))); |
| } |
| |
| Future<void> test_concurrentRequestsCancellation() async { |
| // We expect a new completion request to cancel any in-flight request so |
| // send multiple without awaiting, then check only the last one completes. |
| var code = TestCode.empty; |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| var position = code.position.position; |
| |
| // Use a completer to force the requests to overlap without races. |
| var completer = Completer<void>(); |
| CompletionHandler.delayAfterResolveForTests = completer.future; |
| try { |
| var responseFutures = [ |
| getCompletion(mainFileUri, position), |
| getCompletion(mainFileUri, position), |
| getCompletion(mainFileUri, position), |
| ]; |
| |
| // Ensure all requests started, then let them continue. |
| await pumpEventQueue(times: 5000); |
| completer.complete(); |
| |
| expect(responseFutures[0], |
| throwsA(isResponseError(ErrorCodes.RequestCancelled))); |
| expect(responseFutures[1], |
| throwsA(isResponseError(ErrorCodes.RequestCancelled))); |
| var results = await responseFutures[2]; |
| expect(results, isNotEmpty); |
| } finally { |
| CompletionHandler.delayAfterResolveForTests = null; |
| } |
| } |
| |
| Future<void> test_dartDocPreference_full() => |
| assertDocumentation('full', includesSummary: true, includesFull: true); |
| |
| Future<void> test_dartDocPreference_none() => |
| assertDocumentation('none', includesSummary: false, includesFull: false); |
| |
| Future<void> test_dartDocPreference_summary() => |
| assertDocumentation('summary', |
| includesSummary: true, includesFull: false); |
| |
| /// No preference should result in full docs. |
| Future<void> test_dartDocPreference_unset() => |
| assertDocumentation(null, includesSummary: true, includesFull: true); |
| |
| Future<void> test_filterText_constructorParens() async { |
| // Constructor parens should not be included in filterText. |
| var content = ''' |
| class MyClass {} |
| |
| void f() { |
| MyClass a = new MyCla^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'MyClass()'), isTrue); |
| var item = res.singleWhere((c) => c.label == 'MyClass()'); |
| |
| // filterText is set explicitly because it's not the same as label. |
| expect(item.filterText, 'MyClass'); |
| |
| // The text in the edit should also not contain the parens. |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, 'MyClass'); |
| } |
| |
| Future<void> test_filterText_override_getter() async { |
| // Some completions (eg. overrides) have additional text that is not part |
| // of the label. That text should _not_ appear in filterText as it will |
| // affect the editors relevance ranking as the user types. |
| // https://github.com/dart-lang/sdk/issues/45157 |
| var content = ''' |
| abstract class Person { |
| String get name; |
| } |
| |
| class Student extends Person { |
| nam^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'name => …'); |
| // filterText is set explicitly because it's not the same as label. |
| expect(item.filterText, 'name'); |
| } |
| |
| Future<void> test_filterText_override_method() async { |
| // Some completions (eg. overrides) have additional text that is not part |
| // of the label. That text should _not_ appear in filterText as it will |
| // affect the editors relevance ranking as the user types. |
| // https://github.com/dart-lang/sdk/issues/45157 |
| var content = ''' |
| abstract class Base { |
| void myMethod() {}; |
| } |
| |
| class BaseImpl extends Base { |
| myMet^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'myMethod() { … }'); |
| // filterText is set explicitly because it's not the same as label. |
| expect(item.filterText, 'myMethod'); |
| } |
| |
| Future<void> test_fromPlugin_dartFile() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| var code = TestCode.parse(''' |
| void f() { |
| var x = ''; |
| print(^); |
| } |
| '''); |
| |
| var pluginResult = plugin.CompletionGetSuggestionsResult( |
| code.position.offset, |
| 0, |
| [ |
| plugin.CompletionSuggestion( |
| plugin.CompletionSuggestionKind.INVOCATION, |
| 100, |
| 'x.toUpperCase()', |
| -1, |
| -1, |
| false, |
| false, |
| ), |
| ], |
| ); |
| configureTestPlugin(respondWith: pluginResult); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var fromServer = res.singleWhere((c) => c.label == 'x'); |
| var fromPlugin = res.singleWhere((c) => c.label == 'x.toUpperCase()'); |
| |
| expect(fromServer.kind, equals(CompletionItemKind.Variable)); |
| expect(fromPlugin.kind, equals(CompletionItemKind.Method)); |
| } |
| |
| Future<void> test_fromPlugin_dartFile_withImports() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| var code = TestCode.parse(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| var pluginResult = plugin.CompletionGetSuggestionsResult( |
| code.position.offset, |
| 0, |
| [ |
| plugin.CompletionSuggestion( |
| plugin.CompletionSuggestionKind.IDENTIFIER, |
| 100, |
| 'fooFromDartIO', |
| -1, |
| -1, |
| false, |
| false, |
| libraryUri: 'dart:io', |
| isNotImported: true, |
| ), |
| ], |
| ); |
| configureTestPlugin(respondWith: pluginResult); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| |
| var items = await getCompletion(mainFileUri, code.position.position); |
| var item = items.singleWhere((c) => c.label == 'fooFromDartIO'); |
| var resolved = await resolveCompletion(item); |
| |
| // Apply both the main completion edit and the additionalTextEdits atomically. |
| var newContent = applyTextEdits( |
| code.code, |
| [toTextEdit(resolved.textEdit!)] |
| .followedBy(resolved.additionalTextEdits!) |
| .toList(), |
| ); |
| |
| // Ensure the plugin-supplied import was added. |
| expect(newContent, equals(''' |
| import 'dart:io'; |
| |
| void f() { |
| fooFromDartIO |
| } |
| ''')); |
| } |
| |
| Future<void> test_fromPlugin_nonDartFile() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| var pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.foo'); |
| var pluginAnalyzedFileUri = pathContext.toUri(pluginAnalyzedFilePath); |
| var code = TestCode.parse(''' |
| CREATE TABLE foo ( |
| id INTEGER NOT NULL PRIMARY KEY |
| ); |
| |
| query: SELECT ^ FROM foo; |
| '''); |
| |
| var pluginResult = plugin.CompletionGetSuggestionsResult( |
| code.position.offset, |
| 0, |
| [ |
| plugin.CompletionSuggestion( |
| plugin.CompletionSuggestionKind.IDENTIFIER, |
| 100, |
| 'id', |
| -1, |
| -1, |
| false, |
| false, |
| ), |
| ], |
| ); |
| configureTestPlugin(respondWith: pluginResult); |
| |
| await initialize(); |
| await openFile(pluginAnalyzedFileUri, code.code); |
| var res = |
| await getCompletion(pluginAnalyzedFileUri, code.position.position); |
| |
| expect(res, hasLength(1)); |
| var suggestion = res.single; |
| |
| expect(suggestion.kind, CompletionItemKind.Variable); |
| expect(suggestion.label, equals('id')); |
| } |
| |
| Future<void> test_fromPlugin_tooSlow() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| var code = TestCode.parse(''' |
| void f() { |
| var x = ''; |
| print(^); |
| } |
| '''); |
| |
| var pluginResult = plugin.CompletionGetSuggestionsResult( |
| code.position.offset, |
| 0, |
| [ |
| plugin.CompletionSuggestion( |
| plugin.CompletionSuggestionKind.INVOCATION, |
| 100, |
| 'x.toUpperCase()', |
| -1, |
| -1, |
| false, |
| false, |
| ), |
| ], |
| ); |
| configureTestPlugin( |
| respondWith: pluginResult, |
| // Don't respond within an acceptable time |
| respondAfter: Duration(seconds: 1), |
| ); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var fromServer = res.singleWhere((c) => c.label == 'x'); |
| var fromPlugin = res.singleWhereOrNull((c) => c.label == 'x.toUpperCase()'); |
| |
| // Server results should still be included. |
| expect(fromServer.kind, equals(CompletionItemKind.Variable)); |
| // Plugin results are not because they didn't arrive in time. |
| expect(fromPlugin, isNull); |
| } |
| |
| /// Check that narrowing a type from String? to String in a subclass includes |
| /// the correct narrowed type in the `detail` field. |
| /// |
| /// https://github.com/Dart-Code/Dart-Code/issues/4499 |
| Future<void> test_getter_narrowedBySubclass() async { |
| var content = ''' |
| void f(MyItem item) { |
| item.na^ |
| } |
| |
| abstract class NullableName { |
| String? get name; |
| } |
| |
| abstract class NotNullableName implements NullableName { |
| @override |
| String get name; |
| } |
| |
| abstract class MyItem implements NotNullableName, NullableName {} |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var name = res.singleWhere((c) => c.label == 'name'); |
| expect(name.detail, equals('String')); |
| } |
| |
| Future<void> test_gettersAndSetters() async { |
| var content = ''' |
| class MyClass { |
| String get justGetter => ''; |
| set justSetter(String value) {} |
| String get getterAndSetter => ''; |
| set getterAndSetter(String value) {} |
| } |
| |
| void f() { |
| MyClass a; |
| a.^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var getter = res.singleWhere((c) => c.label == 'justGetter'); |
| var setter = res.singleWhere((c) => c.label == 'justSetter'); |
| var both = res.singleWhere((c) => c.label == 'getterAndSetter'); |
| expect(getter.detail, equals('String')); |
| expect(setter.detail, equals('String')); |
| expect(both.detail, equals('String')); |
| for (var item in [getter, setter, both]) { |
| expect(item.kind, equals(CompletionItemKind.Property)); |
| } |
| } |
| |
| Future<void> test_import() async { |
| var content = ''' |
| import '^'; |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_import_configuration() async { |
| var content = ''' |
| import 'dart:core' if (dart.library.io) '^'; |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_import_configuration_eof() async { |
| var content = ''' |
| import 'dart:core' if (dart.library.io) '^ |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_import_configuration_partial() async { |
| var content = ''' |
| import 'dart:core' if (dart.library.io) 'dart:^'; |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_import_eof() async { |
| var content = ''' |
| import '^ |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_import_partial() async { |
| var content = ''' |
| import 'dart:^'; |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'dart:async'), isTrue); |
| } |
| |
| Future<void> test_insertReplaceRanges() async { |
| setCompletionItemInsertReplaceSupport(); |
| |
| var content = ''' |
| class MyClass { |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^def |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'abcdefghij'), isTrue); |
| var item = res.singleWhere((c) => c.label == 'abcdefghij'); |
| // When using the replacement range, we should get exactly the symbol |
| // we expect. |
| var replaced = applyTextEdits( |
| code.code, |
| [textEditForReplace(item.textEdit!)], |
| ); |
| expect(replaced, contains('a.abcdefghij\n')); |
| // When using the insert range, we should retain what was after the caret |
| // ("def" in this case). |
| var inserted = applyTextEdits( |
| code.code, |
| [textEditForInsert(item.textEdit!)], |
| ); |
| expect(inserted, contains('a.abcdefghijdef\n')); |
| } |
| |
| Future<void> test_insertTextMode_multiline() async { |
| setCompletionItemInsertTextModeSupport(); |
| var content = ''' |
| import 'package:flutter/material.dart'; |
| |
| class _MyWidgetState extends State<MyWidget> { |
| @override |
| Widget build(BuildContext context) { |
| [!setSt^!] |
| return const Placeholder(); |
| } |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label.startsWith('setState')); |
| |
| // Multiline completions should always set insertTextMode.asIs. |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, contains('\n')); |
| expect(item.insertTextMode, equals(InsertTextMode.asIs)); |
| } |
| |
| Future<void> test_insertTextMode_singleLine() async { |
| setCompletionItemInsertTextModeSupport(); |
| |
| var content = ''' |
| void foo() { |
| ^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label.startsWith('foo')); |
| |
| // Single line completions should never set insertTextMode.asIs to |
| // avoid bloating payload size where it wouldn't matter. |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, isNot(contains('\n'))); |
| expect(item.insertTextMode, isNull); |
| } |
| |
| Future<void> test_insideString() async { |
| var content = ''' |
| var a = "This is ^a test" |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res, isEmpty); |
| } |
| |
| Future<void> test_isDeprecated_notSupported() async { |
| var content = ''' |
| class MyClass { |
| @deprecated |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var 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')); |
| } |
| |
| Future<void> test_isDeprecated_supportedFlag() async { |
| setCompletionItemDeprecatedFlagSupport(); |
| var content = ''' |
| class MyClass { |
| @deprecated |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var 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'))); |
| } |
| |
| Future<void> test_isDeprecated_supportedTag() async { |
| setCompletionItemTagSupport([CompletionItemTag.Deprecated]); |
| |
| var content = ''' |
| class MyClass { |
| @deprecated |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var item = res.singleWhere((c) => c.label == 'abcdefghij'); |
| expect(item.tags, contains(CompletionItemTag.Deprecated)); |
| // If the client says it supports the deprecated tag, we should not show |
| // deprecated in the details. |
| expect(item.detail, isNot(contains('deprecated'))); |
| } |
| |
| Future<void> test_isIncomplete_falseIfAllIncluded() async { |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| A a = A(); |
| a.^ |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| // Create a class with fields aaa1 to aaa500 in the other file. |
| newFile( |
| join(projectFolderPath, 'lib', 'a.dart'), |
| [ |
| 'class A {', |
| for (var i = 1; i <= 500; i++) 'String get aaa$i => "";', |
| '}', |
| ].join('\n'), |
| ); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletionList(mainFileUri, code.position.position); |
| |
| // Expect everything (hashCode etc. will take it over 500). |
| expect(res.items, hasLength(greaterThanOrEqualTo(500))); |
| expect(res.isIncomplete, isFalse); |
| } |
| |
| Future<void> test_isIncomplete_trueIfNotAllIncluded() async { |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| A a = A(); |
| a.^ |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| // Create a class with fields aaa1 to aaa500 in the other file. |
| newFile( |
| join(projectFolderPath, 'lib', 'a.dart'), |
| [ |
| 'class A {', |
| for (var i = 1; i <= 500; i++) ' String get aaa$i => "";', |
| ' String get aaa => "";', |
| '}', |
| ].join('\n'), |
| ); |
| |
| await provideConfig(initialize, {'maxCompletionItems': 200}); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletionList(mainFileUri, code.position.position); |
| |
| // Should be capped at 200 and marked as incomplete. |
| expect(res.items, hasLength(200)); |
| expect(res.isIncomplete, isTrue); |
| |
| // Also ensure 'aaa' is included, since relevance sorting should have |
| // put it at the top. |
| expect(res.items.map((item) => item.label).contains('aaa'), isTrue); |
| } |
| |
| Future<void> test_itemDefaults_editRange() async { |
| setCompletionItemInsertReplaceSupport(); |
| setCompletionListDefaults(['editRange']); |
| |
| var content = ''' |
| void myFunction() { |
| [!myFunctio^!] |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| var list = await getCompletionList(mainFileUri, code.position.position); |
| var item = list.items.singleWhere((c) => c.label.startsWith('myFunction')); |
| var defaultEditRange = list.itemDefaults!.editRange!.map( |
| (insertReplace) => throw 'Expected Range, got CompletionItemEditRange', |
| (range) => range, |
| ); |
| |
| // Range covers the ranged marked with [!braces!] in `content`. |
| expect(defaultEditRange, code.range.range); |
| |
| // Item should use the default range. |
| expectUsesDefaultEditRange(item, 'myFunction'); |
| } |
| |
| Future<void> test_itemDefaults_editRange_includesNonDefaultItem() async { |
| setCompletionItemInsertReplaceSupport(); |
| setCompletionListDefaults(['editRange']); |
| |
| // In this code, we will get two completions with different edit ranges: |
| // |
| // - 'b: ' will have a zero-width range because names don't replace args |
| // - 'a' will replace 'b' |
| // |
| // Therefore we expect 'a' to use the default range (and not have its own) |
| // but 'b'` to have its own. |
| // |
| // Additionally, because the caret is before the identifier, we will have |
| // separate default insert/replace ranges. |
| var content = ''' |
| void f(String a, {String? b}) { |
| f([!^b!]); |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| var list = await getCompletionList(mainFileUri, code.position.position); |
| var itemA = list.items.singleWhere((c) => c.label == 'a'); |
| var itemB = list.items.singleWhere((c) => c.label == 'b: '); |
| |
| // Default replace range should span `b`. |
| var expectedRange = code.range.range; |
| var defaultEditRange = list.itemDefaults!.editRange!.map( |
| (insertReplace) => insertReplace, |
| (range) => throw 'Expected Range, got CompletionItemEditRange', |
| ); |
| expect(defaultEditRange.replace, equals(expectedRange)); |
| |
| // Default insert range should be in front of `b`. |
| expect( |
| defaultEditRange.insert, |
| Range(start: expectedRange.start, end: expectedRange.start), |
| ); |
| |
| // And item A should use that default. |
| expectUsesDefaultEditRange(itemA, 'a'); |
| |
| // Item B should have its own range, which is a single range for both |
| // insert and replace that matches the insert range (in front of `b`) of |
| // the default. |
| var itemBTextEdit = toTextEdit(itemB.textEdit!); |
| expect(itemBTextEdit.range, defaultEditRange.insert); |
| expect(itemBTextEdit.newText, 'b: '); |
| } |
| |
| Future<void> test_itemDefaults_textMode() async { |
| setCompletionItemInsertTextModeSupport(); |
| setCompletionListDefaults(['insertTextMode']); |
| |
| // We only normally set InsertTextMode on multiline completions (where it |
| // matters), so ensure there's a multiline completion in the results for |
| // testing. |
| var content = ''' |
| import 'package:flutter/material.dart'; |
| |
| class _MyWidgetState extends State<MyWidget> { |
| @override |
| Widget build(BuildContext context) { |
| [!setSt^!] |
| return const Placeholder(); |
| } |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| await initialize(); |
| await openFile(mainFileUri, code.code); |
| var list = await getCompletionList(mainFileUri, code.position.position); |
| var item = list.items.singleWhere((c) => c.label.startsWith('setState')); |
| |
| // Default should be set. |
| expect(list.itemDefaults?.insertTextMode, InsertTextMode.asIs); |
| // Item should not. |
| expect(item.insertTextMode, isNull); |
| } |
| |
| /// Exact matches should always be included when completion lists are |
| /// truncated, even if they ranked poorly. |
| Future<void> test_maxCompletionItems_doesNotExcludeExactMatches() async { |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| var a = Item^ |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| // Create classes `Item1` to `Item20` along with a field named `item`. |
| // The classes will rank higher in the position above and push |
| // the field out without an exception to include exact matches. |
| newFile( |
| join(projectFolderPath, 'lib', 'a.dart'), |
| [ |
| 'String item = "";', |
| for (var i = 1; i <= 20; i++) 'class Item$i {}', |
| ].join('\n'), |
| ); |
| |
| await provideConfig(initialize, {'maxCompletionItems': 10}); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletionList(mainFileUri, code.position.position); |
| |
| // We expect 11 items, because the exact match was not in the top 10 and |
| // was included additionally. |
| expect(res.items, hasLength(11)); |
| expect(res.isIncomplete, isTrue); |
| |
| // Ensure the 'Item' field is included. |
| expect( |
| res.items.map((item) => item.label), |
| contains('item'), |
| ); |
| } |
| |
| /// Snippet completions should be kept when maxCompletionItems truncates |
| /// because they are not ranked like other completions and might be |
| /// truncated when they are exactly what the user wants. |
| Future<void> test_maxCompletionItems_doesNotExcludeSnippets() async { |
| var content = ''' |
| import 'a.dart'; |
| void f() { |
| fo^ |
| } |
| '''; |
| var code = TestCode.parse(content); |
| |
| // Create fields for1 to for20 in the other file. |
| newFile( |
| join(projectFolderPath, 'lib', 'a.dart'), |
| [ |
| for (var i = 1; i <= 20; i++) 'String for$i = ' ';', |
| ].join('\n'), |
| ); |
| |
| setCompletionItemSnippetSupport(); |
| await provideConfig(initialize, {'maxCompletionItems': 10}); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletionList(mainFileUri, code.position.position); |
| |
| // Should be capped at 10 and marked as incomplete. |
| expect(res.items, hasLength(10)); |
| expect(res.isIncomplete, isTrue); |
| |
| // Ensure the 'for' snippet is included. |
| expect( |
| res.items |
| .where((item) => item.kind == CompletionItemKind.Snippet) |
| .map((item) => item.label) |
| .contains('for'), |
| isTrue, |
| ); |
| } |
| |
| Future<void> test_namedArg_flutterChildren() async { |
| var content = ''' |
| import 'package:flutter/widgets.dart'; |
| |
| final a = Flex(c^); |
| '''; |
| |
| var expectedContent = ''' |
| import 'package:flutter/widgets.dart'; |
| |
| final a = Flex(children: [^],); |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectCompletions: ['children: []'], |
| applyEditsFor: 'children: []', |
| expectedContent: expectedContent, |
| ); |
| } |
| |
| Future<void> test_namedArg_flutterChildren_existingValue() async { |
| // Flutter's widget classes have special handling that adds `[]` after the |
| // children named arg, but this should not occur if there's already a value |
| // for this named arg. |
| var content = ''' |
| import 'package:flutter/widgets.dart'; |
| |
| final a = Flex(c^: []); |
| '''; |
| |
| var expectedContent = ''' |
| import 'package:flutter/widgets.dart'; |
| |
| final a = Flex(children: []); |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectCompletions: ['children'], |
| applyEditsFor: 'children', |
| expectedContent: expectedContent, |
| ); |
| } |
| |
| Future<void> test_namedArg_insertReplaceRanges() async { |
| /// Helper to check multiple completions in the same template file. |
| Future<void> check( |
| String code, |
| String expectedLabel, { |
| required String expectedReplace, |
| required String expectedInsert, |
| }) async { |
| var content = ''' |
| class A { const A({int argOne, int argTwo, String argThree}); } |
| final varOne = ''; |
| $code |
| void f() { } |
| '''; |
| var expectedReplaced = ''' |
| class A { const A({int argOne, int argTwo, String argThree}); } |
| final varOne = ''; |
| $expectedReplace |
| void f() { } |
| '''; |
| var expectedInserted = ''' |
| class A { const A({int argOne, int argTwo, String argThree}); } |
| final varOne = ''; |
| $expectedInsert |
| void f() { } |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectCompletions: [expectedLabel], |
| applyEditsFor: expectedLabel, |
| verifyInsertReplaceRanges: true, |
| expectedContent: expectedReplaced, |
| expectedContentIfInserting: expectedInserted, |
| ); |
| } |
| |
| // When at the start of the identifier, it will be set as the replacement |
| // range so we don't expect the ': ,' |
| await check( |
| '@A(^argOne: 1)', |
| 'argTwo', |
| expectedReplace: '@A(argTwo: 1)', |
| expectedInsert: '@A(argTwoargOne: 1)', |
| ); |
| |
| // When adding a name to an existing value, it should always insert. |
| await check( |
| '@A(^1)', |
| 'argOne: ', |
| expectedReplace: '@A(argOne: 1)', |
| expectedInsert: '@A(argOne: 1)', |
| ); |
| |
| // When adding a name to an existing variable, it should always insert. |
| await check( |
| '@A(argOne: 1, ^varOne)', |
| 'argTwo: ', |
| expectedReplace: '@A(argOne: 1, argTwo: varOne)', |
| expectedInsert: '@A(argOne: 1, argTwo: varOne)', |
| ); |
| |
| // // Inside the identifier also should be expected to replace. |
| await check( |
| '@A(arg^One: 1)', |
| 'argTwo', |
| expectedReplace: '@A(argTwo: 1)', |
| expectedInsert: '@A(argTwoOne: 1)', |
| ); |
| |
| // If there's a space, there's no replacement range so we should still get |
| // the colon/comma since this is always an insert (and both operations will |
| // produce the same text). |
| await check( |
| '@A(^ argOne: 1)', |
| 'argTwo: ', |
| expectedReplace: '@A(argTwo: ^, argOne: 1)', |
| expectedInsert: '@A(argTwo: ^, argOne: 1)', |
| ); |
| |
| // Partially typed names in front of values (that aren't considered part of |
| // the same identifier) should also suggest name labels. |
| await check( |
| '''@A(argOne: 1, argTh^'Foo')''', |
| 'argThree: ', |
| expectedReplace: '''@A(argOne: 1, argThree: 'Foo')''', |
| expectedInsert: '''@A(argOne: 1, argThree: 'Foo')''', |
| ); |
| } |
| |
| Future<void> test_namedArg_offsetBeforeCompletionTarget() async { |
| // This test checks for a previous bug where the completion target was a |
| // symbol far after the cursor offset (`aaaa` here) and caused the whole |
| // identifier to be used as the `targetPrefix` which would filter out |
| // other symbol. |
| // https://github.com/Dart-Code/Dart-Code/issues/2672#issuecomment-666085575 |
| var content = ''' |
| void f() { |
| myFunction( |
| ^ |
| aaaa: '', |
| ); |
| } |
| |
| void myFunction({String aaaa, String aaab, String aaac}) {} |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'aaab: '), isTrue); |
| } |
| |
| Future<void> test_namedArg_plainText() async { |
| var content = ''' |
| class A { const A({int one}); } |
| @A(^) |
| void f() { } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'one: '), isTrue); |
| var item = res.singleWhere((c) => c.label == 'one: '); |
| expect(item.insertTextFormat, |
| anyOf(equals(InsertTextFormat.PlainText), isNull)); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, item.label); |
| var updated = applyTextEdits( |
| code.code, |
| [toTextEdit(item.textEdit!)], |
| ); |
| expect(updated, contains('one: ')); |
| } |
| |
| Future<void> test_namedArg_snippetStringSelection_endOfString() async { |
| var content = ''' |
| class A { const A({int one}); } |
| @A(^) |
| void f() { } |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'one: '), isTrue); |
| var item = res.singleWhere((c) => c.label == 'one: '); |
| // As the selection is the end of the string, there's no need for a snippet |
| // here. Since the insert text is also the same as the label, it does not |
| // need to be provided. |
| expect(item.insertTextFormat, isNull); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, equals('one: ')); |
| expect( |
| textEdit.range, |
| equals(Range(start: code.position.position, end: code.position.position)), |
| ); |
| } |
| |
| Future<void> |
| test_namedArgTrailing_snippetStringSelection_insideString() async { |
| var content = ''' |
| void f({int one, int two}) { |
| f( |
| ^ |
| two: 2, |
| ); |
| } |
| '''; |
| |
| setCompletionItemSnippetSupport(); |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'one: '), isTrue); |
| var item = res.singleWhere((c) => c.label == 'one: '); |
| // Ensure the snippet comes through in the expected format with the expected |
| // placeholder. |
| expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); |
| expect(item.insertText, isNull); |
| var textEdit = toTextEdit(item.textEdit!); |
| expect(textEdit.newText, equals(r'one: $0,')); |
| expect( |
| textEdit.range, |
| equals(Range(start: code.position.position, end: code.position.position)), |
| ); |
| } |
| |
| Future<void> test_nonAnalyzedFile() async { |
| var readmeFilePath = convertPath(join(projectFolderPath, 'README.md')); |
| newFile(readmeFilePath, ''); |
| await initialize(); |
| |
| var res = |
| await getCompletion(pathContext.toUri(readmeFilePath), startOfDocPos); |
| expect(res, isEmpty); |
| } |
| |
| Future<void> test_nullableTypes() async { |
| var content = ''' |
| String? foo(int? a, [int b = 1]) {} |
| |
| void f() { |
| fo^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var completion = res.singleWhere((c) => c.label.startsWith('foo')); |
| expect(completion.detail, '(int? a, [int b = 1]) → String?'); |
| } |
| |
| Future<void> test_plainText() async { |
| var content = ''' |
| class MyClass { |
| String abcdefghij; |
| } |
| |
| void f() { |
| MyClass a; |
| a.abc^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'abcdefghij'), isTrue); |
| var item = res.singleWhere((c) => c.label == 'abcdefghij'); |
| expect(item.insertTextFormat, |
| anyOf(equals(InsertTextFormat.PlainText), isNull)); |
| expect(item.insertText, anyOf(equals('abcdefghij'), isNull)); |
| var updated = applyTextEdits( |
| code.code, |
| [toTextEdit(item.textEdit!)], |
| ); |
| expect(updated, contains('a.abcdefghij')); |
| } |
| |
| Future<void> test_prefixed_enumMember() async { |
| var content = ''' |
| import 'main.dart' as self; |
| |
| enum MyEnum { |
| one, |
| two, |
| } |
| |
| void main() { |
| final x = self.MyEnum.^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'one'), isTrue); |
| } |
| |
| Future<void> test_prefixFilter_endOfSymbol() async { |
| var content = ''' |
| class UniqueNamedClassForLspOne {} |
| class UniqueNamedClassForLspTwo {} |
| class UniqueNamedClassForLspThree {} |
| |
| void f() { |
| // Should match only Two and Three |
| UniqueNamedClassForLspT^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isFalse); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue); |
| } |
| |
| Future<void> test_prefixFilter_midSymbol() async { |
| var content = ''' |
| class UniqueNamedClassForLspOne {} |
| class UniqueNamedClassForLspTwo {} |
| class UniqueNamedClassForLspThree {} |
| |
| void f() { |
| // Should match only Two and Three |
| UniqueNamedClassForLspT^hree |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isFalse); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue); |
| } |
| |
| Future<void> test_prefixFilter_startOfSymbol() async { |
| var content = ''' |
| class UniqueNamedClassForLspOne {} |
| class UniqueNamedClassForLspTwo {} |
| class UniqueNamedClassForLspThree {} |
| |
| void f() { |
| // Should match all three |
| ^UniqueNamedClassForLspT |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isTrue); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue); |
| expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue); |
| } |
| |
| Future<void> test_setters() async { |
| var content = ''' |
| class MyClass { |
| set stringSetter(String a) {} |
| set noArgSetter() {} |
| set multiArgSetter(a, b) {} |
| set functionSetter(String Function(int a, int b) foo) {} |
| } |
| |
| void f() { |
| MyClass a; |
| a.^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var setters = res |
| .where((c) => c.label.endsWith('Setter')) |
| .map((c) => c.detail != null ? '${c.label} (${c.detail})' : c.label) |
| .toList(); |
| expect( |
| setters, |
| [ |
| 'stringSetter (String)', |
| 'noArgSetter', |
| 'multiArgSetter', |
| // Because of how we extract the type name, we don't currently support |
| // this. |
| 'functionSetter', |
| ], |
| ); |
| } |
| |
| Future<void> test_sort_sortsByRelevance() async { |
| var content = ''' |
| class UniquePrefixABC {} |
| class UniquePrefixAaBbCc {} |
| |
| final a = UniquePrefixab^ |
| '''; |
| |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectCompletions: [ |
| // Constructors should all come before the class names, as they have |
| // higher relevance in this position. |
| 'UniquePrefixABC()', |
| 'UniquePrefixAaBbCc()', |
| 'UniquePrefixABC', |
| 'UniquePrefixAaBbCc', |
| ], |
| ); |
| } |
| |
| Future<void> test_sort_truncatesByFuzzyScore() async { |
| var content = ''' |
| class UniquePrefixABC {} |
| class UniquePrefixAaBbCc {} |
| |
| final a = UniquePrefixab^ |
| '''; |
| |
| // Enable truncation after 2 items so we can verify which |
| // items were dropped. |
| await provideConfig(initialize, {'maxCompletionItems': 2}); |
| await verifyCompletions( |
| mainFileUri, |
| content, |
| expectNoAdditionalItems: true, |
| expectCompletions: [ |
| // Although constructors are more relevant, when truncating we will use |
| // fuzzy score, so the closer matches are kept instead and we'll get |
| // constructor+class from the closer match. |
| 'UniquePrefixABC()', |
| 'UniquePrefixABC', |
| ], |
| ); |
| } |
| |
| Future<void> test_unimportedSymbols() async { |
| newFile( |
| join(projectFolderPath, 'other_file.dart'), |
| ''' |
| /// This class is in another file. |
| class InOtherFile {} |
| ''', |
| ); |
| |
| var content = ''' |
| void f() { |
| InOtherF^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| // Find the completion for the class in the other file. |
| var completion = res.singleWhere((c) => c.label == 'InOtherFile'); |
| expect(completion, isNotNull); |
| expect(completion.textEdit, isNotNull); |
| var originalTextEdit = completion.textEdit; |
| |
| // Expect no docs, this is added during resolve. |
| expectDocumentation(completion, isNull); |
| |
| // Resolve the completion item (via server) to get any additional edits. |
| // This is LSP's equiv of getSuggestionDetails() and is invoked by LSP |
| // clients to populate additional info (in our case, any additional edits |
| // for inserting the import). |
| var 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'"), |
| ); |
| |
| // Ensure the doc comment was added. |
| expectDocumentation(resolved, equals('This class is in another file.')); |
| |
| // Ensure the edit did not change. |
| expect(resolved.textEdit, originalTextEdit); |
| |
| // 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. |
| var newContent = applyTextEdits( |
| code.code, |
| [toTextEdit(resolved.textEdit!)] |
| .followedBy(resolved.additionalTextEdits!) |
| .toList(), |
| ); |
| |
| // Ensure both edits were made - the completion, and the inserted import. |
| expect(newContent, equals(''' |
| import '../other_file.dart'; |
| |
| void f() { |
| InOtherFile |
| } |
| ''')); |
| } |
| |
| Future<void> test_unimportedSymbols_dartDocPreference_full() => |
| assertResolvedDocumentation('full', |
| includesSummary: true, includesFull: true); |
| |
| Future<void> test_unimportedSymbols_dartDocPreference_none() => |
| assertResolvedDocumentation('none', |
| includesSummary: false, includesFull: false); |
| |
| Future<void> test_unimportedSymbols_dartDocPreference_summary() => |
| assertResolvedDocumentation('summary', |
| includesSummary: true, includesFull: false); |
| |
| /// No preference should result in full docs. |
| Future<void> test_unimportedSymbols_dartDocPreference_unset() => |
| assertResolvedDocumentation(null, |
| includesSummary: true, includesFull: true); |
| |
| Future<void> |
| test_unimportedSymbols_doesNotDuplicate_importedViaMultipleLibraries() async { |
| // An item that's already imported through multiple libraries that |
| // export it should not result in multiple entries. |
| newFile( |
| join(projectFolderPath, 'lib/source_file.dart'), |
| ''' |
| class MyExportedClass {} |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib/reexport1.dart'), |
| ''' |
| export 'source_file.dart'; |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib/reexport2.dart'), |
| ''' |
| export 'source_file.dart'; |
| ''', |
| ); |
| |
| var content = ''' |
| import 'reexport1.dart'; |
| import 'reexport2.dart'; |
| |
| void f() { |
| MyExported^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| var completions = res.where((c) => c.label == 'MyExportedClass').toList(); |
| expect(completions, hasLength(1)); |
| } |
| |
| Future<void> |
| test_unimportedSymbols_doesNotDuplicate_importedViaSingleLibrary() async { |
| // An item that's already imported through a library that exports it |
| // should not result in multiple entries. |
| newFile( |
| join(projectFolderPath, 'lib/source_file.dart'), |
| ''' |
| class MyExportedClass {} |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib/reexport1.dart'), |
| ''' |
| export 'source_file.dart'; |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib/reexport2.dart'), |
| ''' |
| export 'source_file.dart'; |
| ''', |
| ); |
| |
| var content = ''' |
| import 'reexport1.dart'; |
| |
| void f() { |
| MyExported^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var completions = res.where((c) => c.label == 'MyExportedClass').toList(); |
| expect(completions, hasLength(1)); |
| } |
| |
| Future<void> test_unimportedSymbols_doesNotFilterSymbolsWithSameName() async { |
| // Classes here are not re-exports, so should not be filtered out. |
| newFile( |
| join(projectFolderPath, 'source_file1.dart'), |
| 'class MyDuplicatedClass {}', |
| ); |
| newFile( |
| join(projectFolderPath, 'source_file2.dart'), |
| 'class MyDuplicatedClass {}', |
| ); |
| newFile( |
| join(projectFolderPath, 'source_file3.dart'), |
| 'class MyDuplicatedClass {}', |
| ); |
| |
| var content = ''' |
| void f() { |
| MyDuplicated^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var completions = res.where((c) => c.label == 'MyDuplicatedClass').toList(); |
| expect(completions, hasLength(3)); |
| |
| // Resolve the completions so we can get the auto-import text. |
| var resolvedCompletions = |
| await Future.wait(completions.map(resolveCompletion)); |
| |
| expectAutoImportCompletion(resolvedCompletions, '../source_file1.dart'); |
| expectAutoImportCompletion(resolvedCompletions, '../source_file2.dart'); |
| expectAutoImportCompletion(resolvedCompletions, '../source_file3.dart'); |
| } |
| |
| Future<void> test_unimportedSymbols_enumValues() async { |
| // Enum values only show up in contexts with their types, so we need two |
| // extra files - one with the Enum definition, and one with a function that |
| // accepts the Enum type that is imported into the test files. |
| newFile( |
| join(projectFolderPath, 'lib', 'enum.dart'), |
| ''' |
| enum MyExportedEnum { One, Two } |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib', 'function_x.dart'), |
| ''' |
| import 'package:test/enum.dart'; |
| void x(MyExportedEnum e) {} |
| ''', |
| ); |
| |
| var content = ''' |
| import 'package:test/function_x.dart'; |
| |
| void f() { |
| x(MyExported^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
| var enumCompletions = |
| res.where((c) => c.label.startsWith('MyExportedEnum')).toList(); |
| expect( |
| enumCompletions.map((c) => c.label), |
| unorderedEquals( |
| ['MyExportedEnum', 'MyExportedEnum.One', 'MyExportedEnum.Two'])); |
| |
| var completion = |
| enumCompletions.singleWhere((c) => c.label == 'MyExportedEnum.One'); |
| |
| // 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). |
| var 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 'package:test/enum.dart'"), |
| ); |
| |
| // Ensure the edit was added on. |
| expect(resolved.textEdit, isNotNull); |
| |
| // Apply both the main completion edit and the additionalTextEdits atomically. |
| var newContent = applyTextEdits( |
| code.code, |
| [toTextEdit(resolved.textEdit!)] |
| .followedBy(resolved.additionalTextEdits!) |
| .toList(), |
| ); |
| |
| // Ensure both edits were made - the completion, and the inserted import. |
| expect(newContent, equals(''' |
| import 'package:test/enum.dart'; |
| import 'package:test/function_x.dart'; |
| |
| void f() { |
| x(MyExportedEnum.One |
| } |
| ''')); |
| } |
| |
| Future<void> test_unimportedSymbols_enumValuesAlreadyImported() async { |
| newFile( |
| join(projectFolderPath, 'lib', 'enum.dart'), |
| ''' |
| enum MyExportedEnum { One, Two } |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib', 'reexport1.dart'), |
| ''' |
| import 'enum.dart'; |
| export 'enum.dart'; |
| void x(MyExportedEnum e) {} |
| ''', |
| ); |
| newFile( |
| join(projectFolderPath, 'lib', 'reexport2.dart'), |
| ''' |
| export 'enum.dart'; |
| ''', |
| ); |
| |
| var content = ''' |
| import 'reexport1.dart'; |
| |
| void f() { |
| x(MyExported^ |
| } |
| '''; |
| |
| await initialize(); |
| var code = TestCode.parse(content); |
| await openFile(mainFileUri, code.code); |
| await initialAnalysis; |
| var res = await getCompletion(mainFileUri, code.position.position); |
| |
|