| // Copyright (c) 2020, 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/src/cider/completion.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| import 'package:analyzer/src/test_utilities/function_ast_visitor.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' |
| show CompletionSuggestion, CompletionSuggestionKind, ElementKind; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import 'cider_service.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(CiderCompletionComputerTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class CiderCompletionComputerTest extends CiderServiceTest { |
| final CiderCompletionCache _completionCache = CiderCompletionCache(); |
| |
| late CiderCompletionComputer _computer; |
| void Function(ResolvedUnitResult)? _testResolvedUnit; |
| |
| late CiderCompletionResult _completionResult; |
| late List<CompletionSuggestion> _suggestions; |
| |
| Future<void> test_compute() async { |
| await _compute(r''' |
| class A {} |
| |
| int a = 0; |
| |
| void f(int b) { |
| int c = 0; |
| ^ |
| } |
| '''); |
| |
| _assertHasCompletion(text: 'a'); |
| _assertHasCompletion(text: 'b'); |
| _assertHasCompletion(text: 'c'); |
| _assertHasClass(text: 'A'); |
| _assertHasClass(text: 'String'); |
| |
| _assertNoClass(text: 'Random'); |
| } |
| |
| Future<void> test_compute_performance_operations() async { |
| await _compute(r''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| _assertHasOperationPerformance(['resolution']); |
| _assertHasOperationPerformance(['suggestions']); |
| _assertHasOperationPerformance([ |
| 'suggestions', |
| 'DartCompletionManager - KeywordContributor', |
| ]); |
| } |
| |
| Future<void> test_compute_prefixStart_beforeToken_identifier() async { |
| await _compute(''' |
| const foo = 0; |
| |
| class A { |
| @fo^ |
| } |
| '''); |
| expect(_completionResult.prefixStart.line, 3); |
| expect(_completionResult.prefixStart.column, 3); |
| } |
| |
| Future<void> test_compute_prefixStart_beforeToken_keyword() async { |
| await _compute(''' |
| import 'dart:async' h^; |
| '''); |
| _assertHasKeyword(text: 'hide'); |
| _assertNoKeyword(text: 'show'); |
| expect(_completionResult.prefixStart.line, 0); |
| expect(_completionResult.prefixStart.column, 20); |
| } |
| |
| Future<void> test_compute_prefixStart_identifier() async { |
| await _compute(''' |
| class A { |
| String foobar; |
| } |
| |
| void f(A a) { |
| a.foo^ |
| } |
| '''); |
| expect(_completionResult.prefixStart.line, 5); |
| expect(_completionResult.prefixStart.column, 4); |
| } |
| |
| Future<void> test_compute_prefixStart_noPrefix() async { |
| await _compute(r''' |
| import 'dart:math'; |
| ^ |
| '''); |
| expect(_completionResult.prefixStart.line, 1); |
| expect(_completionResult.prefixStart.column, 0); |
| } |
| |
| Future<void> test_compute_updateImportedLibrary() async { |
| var aPath = convertPath('/workspace/dart/test/lib/a.dart'); |
| newFile(aPath, content: r''' |
| class A {} |
| '''); |
| |
| var content = r''' |
| import 'a.dart'; |
| ^ |
| '''; |
| |
| await _compute(content); |
| _assertComputedImportedLibraries([aPath], hasCore: true); |
| _assertHasClass(text: 'A'); |
| |
| // Repeat the query, still has 'A'. |
| _createFileResolver(); |
| await _compute(content); |
| _assertComputedImportedLibraries([]); |
| _assertHasClass(text: 'A'); |
| |
| // Update the imported library, has 'B', but not 'A'. |
| newFile(aPath, content: r''' |
| class B {} |
| '''); |
| _createFileResolver(); |
| await _compute(content); |
| _assertComputedImportedLibraries([aPath]); |
| _assertHasClass(text: 'B'); |
| _assertNoClass(text: 'A'); |
| } |
| |
| Future<void> test_compute_updateImports() async { |
| var aPath = convertPath('/workspace/dart/test/lib/a.dart'); |
| newFile(aPath, content: r''' |
| class A {} |
| '''); |
| |
| await _compute(r''' |
| var a = ^; |
| '''); |
| _assertComputedImportedLibraries([], hasCore: true); |
| _assertHasClass(text: 'String'); |
| |
| // Repeat the query, still has 'A'. |
| _createFileResolver(); |
| await _compute(r''' |
| import 'a.dart'; |
| var a = ^; |
| '''); |
| _assertComputedImportedLibraries([aPath]); |
| _assertHasClass(text: 'A'); |
| _assertHasClass(text: 'String'); |
| } |
| |
| Future<void> test_compute_uriContributor_disabled() async { |
| newFile('/workspace/dart/test/lib/a.dart', content: ''); |
| await _compute(r''' |
| import '^'; |
| '''); |
| expect(_suggestions, isEmpty); |
| } |
| |
| Future<void> test_filterSort_byPattern_excludeNotMatching() async { |
| await _compute(r''' |
| var a = F^; |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_beforeMethod() async { |
| await _compute(r''' |
| class A { |
| F^ |
| void foo() {} |
| } |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_functionReturnType() async { |
| await _compute(r''' |
| F^ foo() {} |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_methodReturnType() async { |
| await _compute(r''' |
| class A { |
| F^ foo() {} |
| } |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_parameterType() async { |
| await _compute(r''' |
| void foo(F^ a) {} |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_parameterType2() async { |
| await _compute(r''' |
| void foo(^a) {} |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertHasClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_location_statement() async { |
| await _compute(r''' |
| void f() { |
| F^ |
| 0; |
| } |
| '''); |
| |
| _assertHasClass(text: 'Future'); |
| _assertNoClass(text: 'String'); |
| } |
| |
| Future<void> test_filterSort_byPattern_preferPrefix() async { |
| await _compute(r''' |
| class Foobar {} |
| class Falcon {} |
| var a = Fo^; |
| '''); |
| |
| _assertOrder([ |
| _assertHasClass(text: 'Foobar'), |
| _assertHasClass(text: 'Falcon'), |
| ]); |
| } |
| |
| Future<void> test_filterSort_namedArgument_hasPrefix() async { |
| await _compute(r''' |
| void foo({int aaa, int bbb}); |
| |
| void f() { |
| foo(a^); |
| } |
| '''); |
| |
| _assertHasNamedArgument(name: 'aaa'); |
| _assertNoNamedArgument(name: 'bbb'); |
| } |
| |
| Future<void> test_filterSort_namedArgument_noPrefix() async { |
| await _compute(r''' |
| void foo({int aaa, int bbb}); |
| |
| void f() { |
| foo(^); |
| } |
| '''); |
| |
| _assertHasNamedArgument(name: 'aaa'); |
| _assertHasNamedArgument(name: 'bbb'); |
| } |
| |
| Future<void> test_filterSort_namedArgument_noPrefix_beforeOther() async { |
| await _compute(r''' |
| void foo({int aaa = 0, int aab = 0}) {} |
| |
| voif f() { |
| foo( |
| ^ |
| aaa: 0, |
| ); |
| } |
| |
| '''); |
| |
| _assertHasNamedArgument(name: 'aab'); |
| } |
| |
| Future<void> test_filterSort_preferLocal() async { |
| await _compute(r''' |
| var a = 0; |
| void f() { |
| var b = 0; |
| var v = ^; |
| } |
| '''); |
| |
| _assertOrder([ |
| _assertHasLocalVariable(text: 'b'), |
| _assertHasTopLevelVariable(text: 'a'), |
| ]); |
| } |
| |
| Future<void> test_filterSort_sortByName() async { |
| await _compute(r''' |
| void f() { |
| var a = 0; |
| var b = 0; |
| var v = ^; |
| } |
| '''); |
| |
| _assertOrder([ |
| _assertHasLocalVariable(text: 'b'), |
| _assertHasLocalVariable(text: 'a'), |
| ]); |
| } |
| |
| Future<void> test_limitedResolution_class_constructor_body() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A<T> { |
| int f = 0; |
| |
| A(int a) : f = 1 { |
| ^ |
| } |
| |
| void foo() { |
| print(0); |
| } |
| } |
| '''); |
| |
| _assertHasClass(text: 'A'); |
| _assertHasClass(text: 'String'); |
| _assertHasConstructor(text: 'A'); |
| _assertHasFunction(text: 'print'); |
| _assertHasMethod(text: 'foo'); |
| _assertHasParameter(text: 'a'); |
| _assertHasTypeParameter(text: 'T'); |
| } |
| |
| Future<void> test_limitedResolution_class_field_startWithType() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A { |
| void foo() { |
| print(0); |
| } |
| |
| Str^ |
| } |
| '''); |
| |
| _assertHasClass(text: 'String'); |
| } |
| |
| Future<void> test_limitedResolution_class_method_body() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A<T> { |
| void foo() { |
| print(0); |
| } |
| |
| void bar<U>(int a) { |
| ^ |
| } |
| } |
| |
| class B {} |
| |
| enum E { e } |
| '''); |
| |
| _assertHasClass(text: 'A'); |
| _assertHasClass(text: 'B'); |
| _assertHasClass(text: 'String'); |
| _assertHasConstructor(text: 'A'); |
| _assertHasConstructor(text: 'B'); |
| _assertHasEnumConstant(text: 'E.e'); |
| _assertHasMethod(text: 'foo'); |
| _assertHasMethod(text: 'bar'); |
| _assertHasParameter(text: 'a'); |
| _assertHasTypeParameter(text: 'T'); |
| _assertHasTypeParameter(text: 'U'); |
| } |
| |
| Future<void> test_limitedResolution_class_method_body2() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A { |
| void foo() {} |
| } |
| |
| abstract class B { |
| A get a; |
| |
| void notResolved() { |
| print(0); |
| } |
| |
| void completionTarget() { |
| a.^; |
| } |
| } |
| '''); |
| |
| _assertHasGetter(text: 'hashCode'); |
| _assertHasMethod(text: 'foo'); |
| } |
| |
| Future<void> test_limitedResolution_class_method_parameterType() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A { |
| void foo() { |
| print(0); |
| } |
| |
| void bar(Str^) {} |
| } |
| '''); |
| |
| _assertHasClass(text: 'String'); |
| } |
| |
| Future<void> |
| test_limitedResolution_class_method_returnType_hasPartial() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A { |
| void foo() { |
| print(0); |
| } |
| |
| Str^ bar() {} |
| } |
| '''); |
| |
| _assertHasClass(text: 'String'); |
| } |
| |
| Future<void> test_limitedResolution_hasPart() async { |
| newFile('/workspace/dart/test/lib/a.dart', content: r''' |
| class A {} |
| '''); |
| |
| await _compute(r''' |
| part 'a.dart'; |
| ^ |
| '''); |
| |
| _assertHasClass(text: 'int'); |
| _assertHasClass(text: 'A'); |
| } |
| |
| Future<void> test_limitedResolution_inPart_partOfName() async { |
| newFile('/workspace/dart/test/lib/a.dart', content: r''' |
| library my_lib; |
| part 'test.dart'; |
| class A {} |
| '''); |
| |
| await _compute(r''' |
| part of my_lib; |
| ^ |
| '''); |
| |
| _assertHasClass(text: 'int'); |
| _assertHasClass(text: 'A'); |
| } |
| |
| Future<void> test_limitedResolution_inPart_partOfUri() async { |
| newFile('/workspace/dart/test/lib/a.dart', content: r''' |
| part 'test.dart'; |
| class A {} |
| '''); |
| |
| await _compute(r''' |
| part of 'a.dart'; |
| ^ |
| '''); |
| |
| _assertHasClass(text: 'int'); |
| _assertHasClass(text: 'A'); |
| } |
| |
| Future<void> test_limitedResolution_inPart_partOfUri_doesNotExist() async { |
| await _compute(r''' |
| part of 'a.dart'; |
| class A {} |
| ^ |
| '''); |
| |
| _assertHasClass(text: 'int'); |
| _assertHasClass(text: 'A'); |
| } |
| |
| Future<void> test_limitedResolution_mixin_method_body() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| class A { |
| void foo() {} |
| } |
| |
| mixin M { |
| A get a; |
| |
| void notResolved() { |
| print(0); |
| } |
| |
| void completionTarget() { |
| a.^; |
| } |
| } |
| '''); |
| |
| _assertHasGetter(text: 'hashCode'); |
| _assertHasMethod(text: 'foo'); |
| } |
| |
| Future<void> test_limitedResolution_path_hasSpace() async { |
| testPath = convertPath('/workspace/dart/test name/lib/a.dart'); |
| await _compute(r''' |
| class A {} |
| ^ |
| '''); |
| |
| _assertHasClass(text: 'int'); |
| _assertHasClass(text: 'A'); |
| } |
| |
| Future<void> test_limitedResolution_unit_function_body() async { |
| _configureToCheckNotResolved( |
| identifiers: {'print'}, |
| ); |
| |
| await _compute(r''' |
| void foo() { |
| print(0); |
| } |
| |
| void bar(int a) { |
| ^ |
| } |
| '''); |
| |
| _assertHasFunction(text: 'foo'); |
| _assertHasParameter(text: 'a'); |
| } |
| |
| Future<void> test_localTypeInference() async { |
| await _compute(r''' |
| void foo() { |
| var a = 0; |
| a.^ |
| } |
| '''); |
| |
| _assertHasGetter(text: 'isEven'); |
| } |
| |
| Future<void> test_warmUp_cachesImportedLibraries() async { |
| var aPath = convertPath('/workspace/dart/test/lib/a.dart'); |
| newFile(aPath, content: r''' |
| class A {} |
| '''); |
| |
| var bPath = convertPath('/workspace/dart/test/lib/b.dart'); |
| newFile(bPath, content: r''' |
| import 'a.dart'; |
| '''); |
| |
| // Pre-cache `a.dart` using import in `b.dart` |
| await _newComputer().warmUp([bPath]); |
| _assertComputedImportedLibraries([aPath], hasCore: true); |
| |
| // Now we complete in `test.dart`, and `a.dart` is already cached. |
| await _compute(r''' |
| import 'a.dart'; |
| ^ |
| '''); |
| _assertComputedImportedLibraries([]); |
| _assertHasClass(text: 'A'); |
| } |
| |
| void _assertComputedImportedLibraries( |
| List<String> expected, { |
| bool hasCore = false, |
| }) { |
| expected = expected.map(convertPath).toList(); |
| |
| if (hasCore) { |
| var corePath = convertPath('/sdk/lib/core/core.dart'); |
| expected.add(corePath); |
| } |
| |
| expect( |
| _computer.computedImportedLibraries, |
| unorderedEquals(expected), |
| ); |
| } |
| |
| CompletionSuggestion _assertHasClass({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.CLASS, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| void _assertHasCompletion({required String text}) { |
| var matching = _matchingCompletions(text: text); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| } |
| |
| CompletionSuggestion _assertHasConstructor({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.CONSTRUCTOR, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasEnumConstant({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.ENUM_CONSTANT, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasFunction({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.FUNCTION, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasGetter({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.GETTER, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasKeyword({required String text}) { |
| var matching = _matchingKeywordCompletions(text: text); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasLocalVariable({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.LOCAL_VARIABLE, |
| ); |
| expect( |
| matching, |
| hasLength(1), |
| reason: 'Expected exactly one completion in $_suggestions', |
| ); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasMethod({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.METHOD, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasNamedArgument({required String name}) { |
| var matching = _matchingNamedArgumentSuggestions(name: name); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| void _assertHasOperationPerformance(List<String> path) { |
| var current = _completionResult.performance.operations; |
| for (var name in path) { |
| var child = current.getChild(name); |
| expect(child, isNotNull, reason: "No '$name' in $current"); |
| current = child!; |
| } |
| } |
| |
| CompletionSuggestion _assertHasParameter({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.PARAMETER, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasTopLevelVariable({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.TOP_LEVEL_VARIABLE, |
| ); |
| expect( |
| matching, |
| hasLength(1), |
| reason: 'Expected exactly one completion in $_suggestions', |
| ); |
| return matching.single; |
| } |
| |
| CompletionSuggestion _assertHasTypeParameter({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.TYPE_PARAMETER, |
| ); |
| expect(matching, hasLength(1), reason: 'Expected exactly one completion'); |
| return matching.single; |
| } |
| |
| void _assertNoClass({required String text}) { |
| var matching = _matchingCompletions( |
| text: text, |
| elementKind: ElementKind.CLASS, |
| ); |
| expect(matching, isEmpty, reason: 'Expected zero completions'); |
| } |
| |
| void _assertNoKeyword({required String text}) { |
| var matching = _matchingKeywordCompletions(text: text); |
| expect(matching, isEmpty, reason: 'Expected zero completions'); |
| } |
| |
| void _assertNoNamedArgument({required String name}) { |
| var matching = _matchingNamedArgumentSuggestions(name: name); |
| expect(matching, isEmpty, reason: 'Expected zero completions'); |
| } |
| |
| void _assertOrder(List<CompletionSuggestion> suggestions) { |
| var lastIndex = -2; |
| for (var suggestion in suggestions) { |
| var index = _suggestions.indexOf(suggestion); |
| expect(index, isNonNegative, reason: '$suggestion'); |
| expect(index, greaterThan(lastIndex), reason: '$suggestion'); |
| lastIndex = index; |
| } |
| } |
| |
| Future _compute(String content) async { |
| var context = _updateFile(content); |
| |
| _completionResult = await _newComputer().compute( |
| path: convertPath(testPath), |
| line: context.line, |
| column: context.character, |
| testResolvedUnit: _testResolvedUnit, |
| ); |
| _suggestions = _completionResult.suggestions; |
| } |
| |
| /// Configure the [CiderCompletionComputer] to check that when resolving |
| /// for completion we don't resolve unnecessary node. |
| void _configureToCheckNotResolved({required Set<String> identifiers}) { |
| _testResolvedUnit = (resolvedUnitResult) { |
| var unit = resolvedUnitResult.unit; |
| unit.accept( |
| FunctionAstVisitor( |
| simpleIdentifier: (node) { |
| if (identifiers.contains(node.name) && node.staticElement != null) { |
| fail('Unexpectedly resolved node: $node'); |
| } |
| }, |
| ), |
| ); |
| }; |
| } |
| |
| /// TODO(scheglov) Implement incremental updating |
| void _createFileResolver() { |
| createFileResolver(); |
| } |
| |
| List<CompletionSuggestion> _matchingCompletions({ |
| required String text, |
| ElementKind? elementKind, |
| }) { |
| return _suggestions.where((e) { |
| if (e.completion != text) { |
| return false; |
| } |
| |
| if (elementKind != null && e.element!.kind != elementKind) { |
| return false; |
| } |
| |
| return true; |
| }).toList(); |
| } |
| |
| List<CompletionSuggestion> _matchingKeywordCompletions({ |
| required String text, |
| }) { |
| return _suggestions.where((e) { |
| if (e.completion != text) { |
| return false; |
| } |
| return e.kind == CompletionSuggestionKind.KEYWORD; |
| }).toList(); |
| } |
| |
| List<CompletionSuggestion> _matchingNamedArgumentSuggestions({ |
| required String name, |
| }) { |
| return _suggestions.where((e) { |
| if (e.kind != CompletionSuggestionKind.NAMED_ARGUMENT) { |
| return false; |
| } |
| |
| if (!e.completion.startsWith('$name:')) { |
| return false; |
| } |
| |
| return true; |
| }).toList(); |
| } |
| |
| CiderCompletionComputer _newComputer() { |
| return _computer = CiderCompletionComputer( |
| logger, |
| _completionCache, |
| fileResolver, |
| ); |
| } |
| |
| _CompletionContext _updateFile(String content) { |
| var offset = content.indexOf('^'); |
| expect(offset, isPositive, reason: 'Expected to find ^'); |
| expect(content.indexOf('^', offset + 1), -1, reason: 'Expected only one ^'); |
| |
| var lineInfo = LineInfo.fromContent(content); |
| var location = lineInfo.getLocation(offset); |
| |
| content = content.substring(0, offset) + content.substring(offset + 1); |
| newFile(testPath, content: content); |
| |
| return _CompletionContext( |
| content, |
| offset, |
| location.lineNumber - 1, |
| location.columnNumber - 1, |
| ); |
| } |
| } |
| |
| class _CompletionContext { |
| final String content; |
| final int offset; |
| final int line; |
| final int character; |
| |
| _CompletionContext(this.content, this.offset, this.line, this.character); |
| } |