// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:analysis_server/lsp_protocol/protocol_generated.dart'; import 'package:analysis_server/src/lsp/constants.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:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../tool/lsp_spec/matchers.dart'; import 'completion.dart'; import 'server_abstract.dart'; void main() { defineReflectiveSuite(() { defineReflectiveTests(CompletionTest); defineReflectiveTests(CompletionTestWithNullSafetyTest); }); } @reflectiveTest class CompletionTest extends AbstractLspAnalysisServerTest with CompletionTestMixin { void expectAutoImportCompletion(List items, String file) { expect( items.singleWhereOrNull( (c) => c.detail?.contains("Auto import from '$file'") ?? false), isNotNull, ); } @override void setUp() { super.setUp(); writePackageConfig( projectFolderPath, flutter: true, ); } Future test_comment() async { final content = ''' // foo ^ main() {} '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res, isEmpty); } Future 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. final content = ''' // foo ^ '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res, isEmpty); } Future 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. final content = '// foo ^'; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res, isEmpty); } Future test_commitCharacter_completionItem() async { await provideConfig( () => initialize( textDocumentCapabilities: withAllSupportedTextDocumentDynamicRegistrations( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'previewCommitCharacters': true}, ); final content = ''' main() { pri^ } '''; await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final print = res.singleWhere((c) => c.label == 'print(…)'); expect(print.commitCharacters, equals(dartCompletionCommitCharacters)); } Future test_commitCharacter_config() async { final registrations = []; // Provide empty config and collect dynamic registrations during // initialization. await provideConfig( () => monitorDynamicRegistrations( registrations, () => initialize( textDocumentCapabilities: withAllSupportedTextDocumentDynamicRegistrations( emptyTextDocumentClientCapabilities), workspaceCapabilities: withDidChangeConfigurationDynamicRegistration( withConfigurationSupport( emptyWorkspaceClientCapabilities))), ), {}, ); 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); 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); expect(options.allCommitCharacters, equals(dartCompletionCommitCharacters)); } Future test_completeFunctionCalls() async { final content = ''' void myFunction(String a, int b, {String c}) {} main() { [[myFu^]] } '''; await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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, equals(r'myFunction(${1:a}, ${2:b})')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } Future 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. final content = ''' import 'package:flutter/material.dart'; class MyWidget extends StatefulWidget { @override _MyWidgetState createState() => _MyWidgetState(); } class _MyWidgetState extends State { @override Widget build(BuildContext context) { [[setSt^]] return Container(); } } '''; await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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, equals('setState(() {\n \${0:}\n \\});')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } Future test_completeFunctionCalls_noRequiredParameters() async { final content = ''' void myFunction({int a}) {} main() { [[myFu^]] } '''; await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label == 'myFunction(…)'); // With no required params, there should still be parens and a tabstop inside. expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); expect(item.insertText, equals(r'myFunction(${0:})')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } Future test_completeFunctionCalls_show() async { final content = ''' import 'dart:math' show mi^ '''; await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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, equals(r'min')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(item.insertText)); } Future test_completeFunctionCalls_suggestionSets() async { final content = ''' main() { [[pri]]^ } '''; final initialAnalysis = waitForAnalysisComplete(); await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport( withApplyEditSupport(emptyWorkspaceClientCapabilities)), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label == 'print(…)'); // Ensure the snippet comes through in the expected format with the expected // placeholders. expect(item.insertTextFormat, equals(InsertTextFormat.Snippet)); expect(item.insertText, equals(r'print(${0:object})')); expect(item.textEdit, isNull); // Ensure the item can be resolved and gets a proper TextEdit. final resolved = await resolveCompletion(item); expect(resolved.textEdit, isNotNull); final textEdit = toTextEdit(resolved.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } Future test_completionKinds_default() async { newFile(join(projectFolderPath, 'file.dart')); newFolder(join(projectFolderPath, 'folder')); final content = "import '^';"; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final file = res.singleWhere((c) => c.label == 'file.dart'); final folder = res.singleWhere((c) => c.label == 'folder/'); final builtin = res.singleWhere((c) => c.label == 'dart:core'); // Default capabilities include File + Module but not Folder. expect(file.kind, equals(CompletionItemKind.File)); // We fall back to Module if Folder isn't supported. expect(folder.kind, equals(CompletionItemKind.Module)); expect(builtin.kind, equals(CompletionItemKind.Module)); } Future test_completionKinds_imports() async { final content = "import '^';"; // Tell the server we support some specific CompletionItemKinds. await initialize( textDocumentCapabilities: withCompletionItemKinds( emptyTextDocumentClientCapabilities, [ CompletionItemKind.File, CompletionItemKind.Folder, CompletionItemKind.Module, ], ), ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final file = res.singleWhere((c) => c.label == 'file.dart'); final folder = res.singleWhere((c) => c.label == 'folder/'); final builtin = res.singleWhere((c) => c.label == 'dart:core'); expect(file.kind, equals(CompletionItemKind.File)); expect(folder.kind, equals(CompletionItemKind.Folder)); expect(builtin.kind, equals(CompletionItemKind.Module)); } Future test_completionKinds_supportedSubset() async { final content = ''' class MyClass { String abcdefghij; } main() { MyClass a; a.abc^ } '''; // Tell the server we only support the Field CompletionItemKind. await initialize( textDocumentCapabilities: withCompletionItemKinds( emptyTextDocumentClientCapabilities, [CompletionItemKind.Field]), ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final kinds = res.map((item) => item.kind).toList(); // Ensure we only get nulls or Fields (the sample code contains Classes). expect( kinds, everyElement(anyOf(isNull, equals(CompletionItemKind.Field))), ); } Future test_completionTrigger_brace_block() async { // Brace should not trigger completion if a normal code block. final content = r''' main () {^} '''; await _checkResultsForTriggerCharacters(content, ['{'], isEmpty); } Future test_completionTrigger_brace_interpolatedStringExpression() async { // Brace should trigger completion if at the start of an interpolated expression final content = r''' var a = '${^'; '''; await _checkResultsForTriggerCharacters(content, [r'{'], isNotEmpty); } Future test_completionTrigger_brace_rawString() async { // Brace should not trigger completion if in a raw string. final content = r''' var a = r'${^'; '''; await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty); } Future test_completionTrigger_brace_string() async { // Brace should not trigger completion if not at the start of an interpolated // expression. final content = r''' var a = '{^'; '''; await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty); } Future test_completionTrigger_quotes_endingString() async { // Completion triggered by a quote ending a string should not return results. final content = "foo(''^);"; await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); } Future test_completionTrigger_quotes_startingImport() async { // Completion triggered by a quote for import should return results. final content = "import '^'"; await _checkResultsForTriggerCharacters(content, ["'", '"'], isNotEmpty); } Future test_completionTrigger_quotes_startingString() async { // Completion triggered by a quote for normal string should not return results. final content = "foo('^');"; await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); } Future test_completionTrigger_quotes_terminatingImport() async { // Completion triggered by a quote ending an import should not return results. final content = "import ''^"; await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty); } Future 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. final content = r''' import 'package:test/^'; '''; await _checkResultsForTriggerCharacters(content, [r'/'], isNotEmpty); } Future test_completionTrigger_slash_divide() async { // Slashes should not trigger completion when typing in a normal expression. final content = r''' var a = 1 /^ '''; await _checkResultsForTriggerCharacters(content, [r'/'], isEmpty); } Future test_completionTriggerKinds_invalidParams() async { await initialize(); final invalidTriggerKind = CompletionTriggerKind.fromJson(-1); final request = getCompletion( mainFileUri, Position(line: 0, character: 0), context: CompletionContext( triggerKind: invalidTriggerKind, triggerCharacter: 'A'), ); await expectLater( request, throwsA(isResponseError(ErrorCodes.InvalidParams))); } Future test_filterTextNotIncludeAdditionalText() 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 final content = ''' abstract class Person { String get name; } class Student extends Person { nam^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhereOrNull((c) => c.label.startsWith('name =>')); expect(item, isNotNull); expect(item!.label, equals('name => …')); expect(item.filterText, isNull); // Falls back to label expect(item.insertText, equals('''@override // TODO: implement name String get name => throw UnimplementedError();''')); } Future test_fromPlugin_dartFile() async { final content = ''' void main() { var x = ''; print(^); } '''; final pluginResult = plugin.CompletionGetSuggestionsResult( content.indexOf('^'), 0, [ plugin.CompletionSuggestion( plugin.CompletionSuggestionKind.INVOCATION, 100, 'x.toUpperCase()', -1, -1, false, false, ), ], ); configureTestPlugin(respondWith: pluginResult); await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final fromServer = res.singleWhere((c) => c.label == 'x'); final fromPlugin = res.singleWhere((c) => c.label == 'x.toUpperCase()'); expect(fromServer.kind, equals(CompletionItemKind.Variable)); expect(fromPlugin.kind, equals(CompletionItemKind.Method)); } Future test_fromPlugin_nonDartFile() async { final pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.foo'); final pluginAnalyzedFileUri = Uri.file(pluginAnalyzedFilePath); final content = ''' CREATE TABLE foo ( id INTEGER NOT NULL PRIMARY KEY ); query: SELECT ^ FROM foo; '''; final pluginResult = plugin.CompletionGetSuggestionsResult( content.indexOf('^'), 0, [ plugin.CompletionSuggestion( plugin.CompletionSuggestionKind.IDENTIFIER, 100, 'id', -1, -1, false, false, ), ], ); configureTestPlugin(respondWith: pluginResult); await initialize(); await openFile(pluginAnalyzedFileUri, withoutMarkers(content)); final res = await getCompletion(pluginAnalyzedFileUri, positionFromMarker(content)); expect(res, hasLength(1)); final suggestion = res.single; expect(suggestion.kind, CompletionItemKind.Variable); expect(suggestion.label, equals('id')); } Future test_fromPlugin_tooSlow() async { final content = ''' void main() { var x = ''; print(^); } '''; final pluginResult = plugin.CompletionGetSuggestionsResult( content.indexOf('^'), 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, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final fromServer = res.singleWhere((c) => c.label == 'x'); final 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); } Future test_gettersAndSetters() async { final content = ''' class MyClass { String get justGetter => ''; String set justSetter(String value) {} String get getterAndSetter => ''; String set getterAndSetter(String value) {} } main() { MyClass a; a.^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final getter = res.singleWhere((c) => c.label == 'justGetter'); final setter = res.singleWhere((c) => c.label == 'justSetter'); final both = res.singleWhere((c) => c.label == 'getterAndSetter'); expect(getter.detail, equals('String')); expect(setter.detail, equals('String')); expect(both.detail, equals('String')); [getter, setter, both].forEach((item) { expect(item.kind, equals(CompletionItemKind.Property)); }); } Future test_insertReplaceRanges() async { final content = ''' class MyClass { String abcdefghij; } main() { MyClass a; a.abc^def } '''; await initialize( textDocumentCapabilities: withCompletionItemInsertReplaceSupport( emptyTextDocumentClientCapabilities), ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'abcdefghij'), isTrue); final item = res.singleWhere((c) => c.label == 'abcdefghij'); // When using the replacement range, we should get exactly the symbol // we expect. final replaced = applyTextEdits( withoutMarkers(content), [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). final inserted = applyTextEdits( withoutMarkers(content), [textEditForInsert(item.textEdit!)], ); expect(inserted, contains('a.abcdefghijdef\n')); } Future test_insertTextMode_multiline() async { final content = ''' import 'package:flutter/material.dart'; class _MyWidgetState extends State { @override Widget build(BuildContext context) { [[setSt^]] return Container(); } } '''; await initialize( textDocumentCapabilities: withCompletionItemInsertTextModeSupport( emptyTextDocumentClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label.startsWith('setState')); // Multiline completions should always set insertTextMode.asIs. expect(item.insertText, contains('\n')); expect(item.insertTextMode, equals(InsertTextMode.asIs)); } Future test_insertTextMode_singleLine() async { final content = ''' main() { ^ } '''; await initialize( textDocumentCapabilities: withCompletionItemInsertTextModeSupport( emptyTextDocumentClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label.startsWith('main')); // Single line completions should never set insertTextMode.asIs to // avoid bloating payload size where it wouldn't matter. expect(item.insertText, isNot(contains('\n'))); expect(item.insertTextMode, isNull); } Future test_insideString() async { final content = ''' var a = "This is ^a test" '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res, isEmpty); } Future test_isDeprecated_notSupported() async { final content = ''' class MyClass { @deprecated String abcdefghij; } main() { MyClass a; a.abc^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label == 'abcdefghij'); expect(item.deprecated, isNull); // If the does not say it supports the deprecated flag, we should show // '(deprecated)' in the details. expect(item.detail!.toLowerCase(), contains('deprecated')); } Future test_isDeprecated_supportedFlag() async { final content = ''' class MyClass { @deprecated String abcdefghij; } main() { MyClass a; a.abc^ } '''; await initialize( textDocumentCapabilities: withCompletionItemDeprecatedFlagSupport( emptyTextDocumentClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final item = res.singleWhere((c) => c.label == 'abcdefghij'); expect(item.deprecated, isTrue); // If the client says it supports the deprecated flag, we should not show // deprecated in the details. expect(item.detail, isNot(contains('deprecated'))); } Future test_isDeprecated_supportedTag() async { final content = ''' class MyClass { @deprecated String abcdefghij; } main() { MyClass a; a.abc^ } '''; await initialize( textDocumentCapabilities: withCompletionItemTagSupport( emptyTextDocumentClientCapabilities, [CompletionItemTag.Deprecated])); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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 test_namedArg_insertReplaceRanges() async { /// Helper to check multiple completions in the same template file. Future check( String code, String expectedLabel, { required String expectedReplace, required String expectedInsert, }) async { final content = ''' class A { const A({int argOne, int argTwo, String argThree}); } final varOne = ''; $code main() { } '''; final expectedReplaced = ''' class A { const A({int argOne, int argTwo, String argThree}); } final varOne = ''; $expectedReplace main() { } '''; final expectedInserted = ''' class A { const A({int argOne, int argTwo, String argThree}); } final varOne = ''; $expectedInsert main() { } '''; 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 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 final content = ''' void main() { myFunction( ^ aaaa: '', ); } void myFunction({String aaaa, String aaab, String aaac}) {} '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'aaab: '), isTrue); } Future test_namedArg_plainText() async { final content = ''' class A { const A({int one}); } @A(^) main() { } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'one: '), isTrue); final item = res.singleWhere((c) => c.label == 'one: '); expect(item.insertTextFormat, anyOf(equals(InsertTextFormat.PlainText), isNull)); expect(item.insertText, anyOf(equals('test'), isNull)); final updated = applyTextEdits( withoutMarkers(content), [toTextEdit(item.textEdit!)], ); expect(updated, contains('one: ')); } Future test_namedArg_snippetStringSelection_endOfString() async { final content = ''' class A { const A({int one}); } @A(^) main() { } '''; await initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'one: '), isTrue); final 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); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals('one: ')); expect( textEdit.range, equals(Range( start: positionFromMarker(content), end: positionFromMarker(content))), ); } Future test_namedArgTrailing_snippetStringSelection_insideString() async { final content = ''' main({int one, int two}) { main( ^ two: 2, ); } '''; await initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'one: '), isTrue); final 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, equals(r'one: ${0:},')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(r'one: ${0:},')); expect( textEdit.range, equals(Range( start: positionFromMarker(content), end: positionFromMarker(content))), ); } Future test_nonAnalyzedFile() async { final readmeFilePath = convertPath(join(projectFolderPath, 'README.md')); newFile(readmeFilePath, content: ''); await initialize(); final res = await getCompletion(Uri.file(readmeFilePath), startOfDocPos); expect(res, isEmpty); } Future test_parensNotInFilterTextInsertText() async { final content = ''' class MyClass {} main() { MyClass a = new MyCla^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'MyClass()'), isTrue); final item = res.singleWhere((c) => c.label == 'MyClass()'); expect(item.filterText, equals('MyClass')); expect(item.insertText, equals('MyClass')); } Future test_plainText() async { final content = ''' class MyClass { String abcdefghij; } main() { MyClass a; a.abc^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'abcdefghij'), isTrue); final item = res.singleWhere((c) => c.label == 'abcdefghij'); expect(item.insertTextFormat, anyOf(equals(InsertTextFormat.PlainText), isNull)); expect(item.insertText, anyOf(equals('abcdefghij'), isNull)); final updated = applyTextEdits( withoutMarkers(content), [toTextEdit(item.textEdit!)], ); expect(updated, contains('a.abcdefghij')); } Future test_prefixFilter_endOfSymbol() async { final content = ''' class UniqueNamedClassForLspOne {} class UniqueNamedClassForLspTwo {} class UniqueNamedClassForLspThree {} main() { // Should match only Two and Three UniqueNamedClassForLspT^ } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); 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 test_prefixFilter_midSymbol() async { final content = ''' class UniqueNamedClassForLspOne {} class UniqueNamedClassForLspTwo {} class UniqueNamedClassForLspThree {} main() { // Should match only Two and Three UniqueNamedClassForLspT^hree } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); 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 test_prefixFilter_startOfSymbol() async { final content = ''' class UniqueNamedClassForLspOne {} class UniqueNamedClassForLspTwo {} class UniqueNamedClassForLspThree {} main() { // Should match all three ^UniqueNamedClassForLspT } '''; await initialize(); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); 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 test_suggestionSets() async { newFile( join(projectFolderPath, 'other_file.dart'), content: ''' /// This class is in another file. class InOtherFile {} ''', ); final content = ''' main() { InOtherF^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); // Find the completion for the class in the other file. final completion = res.singleWhere((c) => c.label == 'InOtherFile'); expect(completion, isNotNull); // Expect no docs or text edit, since these are added during resolve. expect(completion.documentation, isNull); expect(completion.textEdit, isNull); // Resolve the completion item (via server) to get its edits. This is the // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to // populate additional info (in our case, the additional edits for inserting // the import). final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Ensure the detail field was update to show this will auto-import. expect( resolved.detail, startsWith("Auto import from '../other_file.dart'")); // Ensure the doc comment was added. expect( resolved.documentation!.valueEquals('This class is in another file.'), isTrue, ); // Ensure the edit was added on. expect(resolved.textEdit, isNotNull); // There should be no command for this item because it doesn't need imports // in other files. Same-file completions are in additionalEdits. expect(resolved.command, isNull); // Apply both the main completion edit and the additionalTextEdits atomically. final newContent = applyTextEdits( withoutMarkers(content), [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'; main() { InOtherFile } ''')); } Future test_suggestionSets_doesNotFilterSymbolsWithSameName() async { // Classes here are not re-exports, so should not be filtered out. newFile( join(projectFolderPath, 'source_file1.dart'), content: 'class MyDuplicatedClass {}', ); newFile( join(projectFolderPath, 'source_file2.dart'), content: 'class MyDuplicatedClass {}', ); newFile( join(projectFolderPath, 'source_file3.dart'), content: 'class MyDuplicatedClass {}', ); final content = ''' main() { MyDuplicated^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completions = res.where((c) => c.label == 'MyDuplicatedClass').toList(); expect(completions, hasLength(3)); // Resolve the completions so we can get the auto-import text. final resolvedCompletions = await Future.wait(completions.map(resolveCompletion)); expectAutoImportCompletion(resolvedCompletions, '../source_file1.dart'); expectAutoImportCompletion(resolvedCompletions, '../source_file2.dart'); expectAutoImportCompletion(resolvedCompletions, '../source_file3.dart'); } Future test_suggestionSets_enumValues() async { newFile( join(projectFolderPath, 'source_file.dart'), content: ''' enum MyExportedEnum { One, Two } ''', ); final content = ''' main() { var a = MyExported^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final enumCompletions = res.where((c) => c.label.startsWith('MyExportedEnum')).toList(); expect( enumCompletions.map((c) => c.label), unorderedEquals( ['MyExportedEnum', 'MyExportedEnum.One', 'MyExportedEnum.Two'])); final 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). final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Ensure the detail field was update to show this will auto-import. expect( resolved.detail, startsWith("Auto import from '../source_file.dart'")); // Ensure the edit was added on. expect(resolved.textEdit, isNotNull); // Apply both the main completion edit and the additionalTextEdits atomically. final newContent = applyTextEdits( withoutMarkers(content), [toTextEdit(resolved.textEdit!)] .followedBy(resolved.additionalTextEdits!) .toList(), ); // Ensure both edits were made - the completion, and the inserted import. expect(newContent, equals(''' import '../source_file.dart'; main() { var a = MyExportedEnum.One } ''')); } Future test_suggestionSets_enumValuesAlreadyImported() async { newFile( join(projectFolderPath, 'lib', 'source_file.dart'), content: ''' enum MyExportedEnum { One, Two } ''', ); newFile( join(projectFolderPath, 'lib', 'reexport1.dart'), content: ''' export 'source_file.dart'; ''', ); newFile( join(projectFolderPath, 'lib', 'reexport2.dart'), content: ''' export 'source_file.dart'; ''', ); final content = ''' import 'reexport1.dart'; main() { var a = MyExported^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completions = res.where((c) => c.label == 'MyExportedEnum.One').toList(); expect(completions, hasLength(1)); final resolved = await resolveCompletion(completions.first); // It should not include auto-import text since it's already imported. expect(resolved.detail, isNull); } Future test_suggestionSets_filtersOutAlreadyImportedSymbols() async { newFile( join(projectFolderPath, 'lib', 'source_file.dart'), content: ''' class MyExportedClass {} ''', ); newFile( join(projectFolderPath, 'lib', 'reexport1.dart'), content: ''' export 'source_file.dart'; ''', ); newFile( join(projectFolderPath, 'lib', 'reexport2.dart'), content: ''' export 'source_file.dart'; ''', ); final content = ''' import 'reexport1.dart'; main() { MyExported^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completions = res.where((c) => c.label == 'MyExportedClass').toList(); expect(completions, hasLength(1)); final resolved = await resolveCompletion(completions.first); // It should not include auto-import text since it's already imported. expect(resolved.detail, isNull); } Future test_suggestionSets_includesReexportedSymbolsForEachFile() async { newFile( join(projectFolderPath, 'source_file.dart'), content: ''' class MyExportedClass {} ''', ); newFile( join(projectFolderPath, 'reexport1.dart'), content: ''' export 'source_file.dart'; ''', ); newFile( join(projectFolderPath, 'reexport2.dart'), content: ''' export 'source_file.dart'; ''', ); final content = ''' main() { MyExported^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completions = res.where((c) => c.label == 'MyExportedClass').toList(); expect(completions, hasLength(3)); // Resolve the completions so we can get the auto-import text. final resolvedCompletions = await Future.wait(completions.map(resolveCompletion)); expectAutoImportCompletion(resolvedCompletions, '../source_file.dart'); expectAutoImportCompletion(resolvedCompletions, '../reexport1.dart'); expectAutoImportCompletion(resolvedCompletions, '../reexport2.dart'); } Future test_suggestionSets_insertReplaceRanges() async { newFile( join(projectFolderPath, 'other_file.dart'), content: ''' /// This class is in another file. class InOtherFile {} ''', ); final content = ''' main() { InOtherF^il } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( textDocumentCapabilities: withCompletionItemInsertReplaceSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities), ); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); // Find the completion for the class in the other file. final completion = res.singleWhere((c) => c.label == 'InOtherFile'); expect(completion, isNotNull); // Expect no docs or text edit, since these are added during resolve. expect(completion.documentation, isNull); expect(completion.textEdit, isNull); // Resolve the completion item (via server) to get its edits. This is the // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to // populate additional info (in our case, the additional edits for inserting // the import). final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Ensure the detail field was update to show this will auto-import. expect( resolved.detail, startsWith("Auto import from '../other_file.dart'")); // Ensure the doc comment was added. expect( resolved.documentation!.valueEquals('This class is in another file.'), isTrue, ); // Ensure the edit was added on. expect(resolved.textEdit, isNotNull); // 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 // then check the contents. final newContentReplaceMode = applyTextEdits( withoutMarkers(content), [textEditForReplace(resolved.textEdit!)] .followedBy(resolved.additionalTextEdits!) .toList(), ); final newContentInsertMode = applyTextEdits( withoutMarkers(content), [textEditForInsert(resolved.textEdit!)] .followedBy(resolved.additionalTextEdits!) .toList(), ); // Ensure both edits were made - the completion, and the inserted import. expect(newContentReplaceMode, equals(''' import '../other_file.dart'; main() { InOtherFile } ''')); // In insert mode, we'd have the trailing "il" still after the caret. expect(newContentInsertMode, equals(''' import '../other_file.dart'; main() { InOtherFileil } ''')); } Future test_suggestionSets_insertsIntoPartFiles() async { // File we'll be adding an import for. newFile( join(projectFolderPath, 'other_file.dart'), content: 'class InOtherFile {}', ); // File that will have the import added. final parentContent = '''part 'main.dart';'''; final parentFilePath = newFile( join(projectFolderPath, 'lib', 'parent.dart'), content: parentContent, ).path; // File that we're invoking completion in. final content = ''' part of 'parent.dart'; main() { InOtherF^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completion = res.singleWhere((c) => c.label == 'InOtherFile'); expect(completion, isNotNull); // Resolve the completion item to get its edits. final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Ensure it has a command, since it will need to make edits in other files // and that's done by telling the server to send a workspace/applyEdit. LSP // doesn't currently support these other-file edits in the completion. // See https://github.com/microsoft/language-server-protocol/issues/749 expect(resolved.command, isNotNull); // Apply all current-document edits. final newContent = applyTextEdits( withoutMarkers(content), [toTextEdit(resolved.textEdit!)] .followedBy(resolved.additionalTextEdits!) .toList(), ); expect(newContent, equals(''' part of 'parent.dart'; main() { InOtherFile } ''')); // Execute the associated command (which will handle edits in other files). ApplyWorkspaceEditParams? editParams; final commandResponse = await handleExpectedRequest( Method.workspace_applyEdit, ApplyWorkspaceEditParams.fromJson, () => executeCommand(resolved.command!), handler: (edit) { // When the server sends the edit back, just keep a copy and say we // applied successfully (it'll be verified below). editParams = edit; return ApplyWorkspaceEditResponse(applied: true); }, ); // Successful edits return an empty success() response. expect(commandResponse, isNull); // Ensure the edit came back. expect(editParams, isNotNull); expect(editParams!.edit.changes, isNotNull); // Ensure applying the changes will give us the expected content. final contents = { parentFilePath: withoutMarkers(parentContent), }; applyChanges(contents, editParams!.edit.changes!); // Check the parent file was modified to include the import by the edits // that came from the server. expect(contents[parentFilePath], equals(''' import '../other_file.dart'; part 'main.dart';''')); } Future test_suggestionSets_members() async { newFile( join(projectFolderPath, 'source_file.dart'), content: ''' class MyExportedClass { DateTime myInstanceDateTime; static DateTime myStaticDateTimeField; static DateTime get myStaticDateTimeGetter => null; } ''', ); final content = ''' main() { var a = MyExported^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final completions = res.where((c) => c.label.startsWith('MyExportedClass')).toList(); expect( completions.map((c) => c.label), unorderedEquals([ 'MyExportedClass', 'MyExportedClass()', // The instance field should not show up. 'MyExportedClass.myStaticDateTimeField', 'MyExportedClass.myStaticDateTimeGetter' ])); final completion = completions .singleWhere((c) => c.label == 'MyExportedClass.myStaticDateTimeField'); // Resolve the completion item (via server) to get its edits. This is the // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to // populate additional info (in our case, the additional edits for inserting // the import). final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Ensure the detail field was update to show this will auto-import. expect( resolved.detail, startsWith("Auto import from '../source_file.dart'")); // Ensure the edit was added on. expect(resolved.textEdit, isNotNull); // Apply both the main completion edit and the additionalTextEdits atomically. final newContent = applyTextEdits( withoutMarkers(content), [toTextEdit(resolved.textEdit!)] .followedBy(resolved.additionalTextEdits!) .toList(), ); // Ensure both edits were made - the completion, and the inserted import. expect(newContent, equals(''' import '../source_file.dart'; main() { var a = MyExportedClass.myStaticDateTimeField } ''')); } /// This test reproduces a bug where the pathKey hash used in /// available_declarations.dart would not change with the contents of the file /// (as it always used 0 as the modification stamp) which would prevent /// completion including items from files that were open (had overlays). /// https://github.com/Dart-Code/Dart-Code/issues/2286#issuecomment-658597532 Future test_suggestionSets_modifiedFiles() async { final otherFilePath = join(projectFolderPath, 'lib', 'other_file.dart'); final otherFileUri = Uri.file(otherFilePath); final mainFileContent = 'MyOtherClass^'; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(mainFileContent)); await initialAnalysis; // Start with a blank file. newFile(otherFilePath, content: ''); await openFile(otherFileUri, ''); await pumpEventQueue(times: 5000); // Reopen the file with a class definition. await closeFile(otherFileUri); await openFile(otherFileUri, 'class MyOtherClass {}'); await pumpEventQueue(times: 5000); // Ensure the class appears in completion. final completions = await getCompletion(mainFileUri, positionFromMarker(mainFileContent)); final matching = completions.where((c) => c.label == 'MyOtherClass').toList(); expect(matching, hasLength(1)); } Future test_suggestionSets_namedConstructors() async { newFile( join(projectFolderPath, 'other_file.dart'), content: ''' /// This class is in another file. class InOtherFile { InOtherFile.fromJson() {} } ''', ); final content = ''' main() { var a = InOtherF^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize( workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities)); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); // Find the completion for the class in the other file. final completion = res.singleWhere((c) => c.label == 'InOtherFile.fromJson()'); expect(completion, isNotNull); // Expect no docs or text edit, since these are added during resolve. expect(completion.documentation, isNull); expect(completion.textEdit, isNull); // Resolve the completion item (via server) to get its edits. This is the // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to // populate additional info (in our case, the additional edits for inserting // the import). final resolved = await resolveCompletion(completion); expect(resolved, isNotNull); // Apply both the main completion edit and the additionalTextEdits atomically. final newContent = applyTextEdits( withoutMarkers(content), [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'; main() { var a = InOtherFile.fromJson } ''')); } Future test_suggestionSets_unavailableIfDisabled() async { newFile( join(projectFolderPath, 'other_file.dart'), content: 'class InOtherFile {}', ); final content = ''' main() { InOtherF^ } '''; final initialAnalysis = waitForAnalysisComplete(); // Support applyEdit, but explicitly disable the suggestions. await initialize( initializationOptions: {'suggestFromUnimportedLibraries': false}, workspaceCapabilities: withApplyEditSupport(emptyWorkspaceClientCapabilities), ); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); // Ensure the item doesn't appear in the results (because we might not // be able to execute the import edits if they're in another file). final completion = res.singleWhereOrNull((c) => c.label == 'InOtherFile'); expect(completion, isNull); } Future test_suggestionSets_unavailableWithoutApplyEdit() async { // If client doesn't advertise support for workspace/applyEdit, we won't // include suggestion sets. newFile( join(projectFolderPath, 'other_file.dart'), content: 'class InOtherFile {}', ); final content = ''' main() { InOtherF^ } '''; final initialAnalysis = waitForAnalysisComplete(); await initialize(); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); // Ensure the item doesn't appear in the results (because we might not // be able to execute the import edits if they're in another file). final completion = res.singleWhereOrNull((c) => c.label == 'InOtherFile'); expect(completion, isNull); } Future test_unopenFile() async { final content = ''' class MyClass { String abcdefghij; } main() { MyClass a; a.abc^ } '''; newFile(mainFilePath, content: withoutMarkers(content)); await initialize(); final res = await getCompletion(mainFileUri, positionFromMarker(content)); expect(res.any((c) => c.label == 'abcdefghij'), isTrue); final item = res.singleWhere((c) => c.label == 'abcdefghij'); expect(item.insertTextFormat, anyOf(equals(InsertTextFormat.PlainText), isNull)); expect(item.insertText, anyOf(equals('abcdefghij'), isNull)); final updated = applyTextEdits( withoutMarkers(content), [toTextEdit(item.textEdit!)], ); expect(updated, contains('a.abcdefghij')); } Future _checkResultsForTriggerCharacters(String content, List triggerCharacters, Matcher expectedResults) async { await initialize(); await openFile(mainFileUri, withoutMarkers(content)); for (final triggerCharacter in triggerCharacters) { final context = CompletionContext( triggerKind: CompletionTriggerKind.TriggerCharacter, triggerCharacter: triggerCharacter); final res = await getCompletion(mainFileUri, positionFromMarker(content), context: context); expect(res, expectedResults); } } } @reflectiveTest class CompletionTestWithNullSafetyTest extends AbstractLspAnalysisServerTest { @override String get testPackageLanguageVersion => latestLanguageVersion; Future test_completeFunctionCalls_requiredNamed() async { final content = ''' void myFunction(String a, int b, {required String c, String d = ''}) {} main() { [[myFu^]] } '''; await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withConfigurationSupport(emptyWorkspaceClientCapabilities), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})')); final textEdit = toTextEdit(item.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } Future test_completeFunctionCalls_requiredNamed_suggestionSet() async { final otherFile = join(projectFolderPath, 'lib', 'other.dart'); newFile( otherFile, content: "void myFunction(String a, int b, {required String c, String d = ''}) {}", ); final content = ''' main() { [[myFu^]] } '''; final initialAnalysis = waitForAnalysisComplete(); await provideConfig( () => initialize( textDocumentCapabilities: withCompletionItemSnippetSupport( emptyTextDocumentClientCapabilities), workspaceCapabilities: withApplyEditSupport( withConfigurationSupport(emptyWorkspaceClientCapabilities)), ), {'completeFunctionCalls': true}, ); await openFile(mainFileUri, withoutMarkers(content)); await initialAnalysis; final res = await getCompletion(mainFileUri, positionFromMarker(content)); final 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, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})')); expect(item.textEdit, isNull); // Ensure the item can be resolved and gets a proper TextEdit. final resolved = await resolveCompletion(item); expect(resolved.textEdit, isNotNull); final textEdit = toTextEdit(resolved.textEdit!); expect(textEdit.newText, equals(item.insertText)); expect(textEdit.range, equals(rangeFromMarkers(content))); } }