blob: 165262a3521555b5a4c987d312eeee108044ec59 [file] [log] [blame]
// Copyright (c) 2022, 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/protocol_server.dart';
import 'package:analysis_server/src/services/snippets/dart_snippet_request.dart';
import 'package:analysis_server/src/services/snippets/snippet.dart';
import 'package:analysis_server/src/services/snippets/snippet_manager.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import '../../../abstract_single_unit.dart';
abstract class DartSnippetProducerTest extends AbstractSingleUnitTest {
SnippetProducerGenerator get generator;
String get label;
String get prefix;
/// Override the package root because it usually contains /test/ and some
/// snippets behave differently for test files.
@override
String get testPackageRootPath => '$workspaceRootPath/my_package';
@override
bool get verifyNoTestUnitErrors => false;
String applySnippet(TestCode code, Snippet snippet) {
var result = code.code;
for (var edit in snippet.change.edits) {
result = SourceEdit.applySequence(result, edit.edits);
}
return result;
}
Future<void> assertSnippet(String content, String expected) async {
var code = TestCode.parse(content);
var expectedCode = TestCode.parse(expected);
var snippet = await expectValidSnippet(code);
expect(snippet.prefix, prefix);
expect(snippet.label, label);
expect(snippet.change.edits, hasLength(1));
// Apply the edits and check the results.
var codeResult = code.code;
for (var edit in snippet.change.edits) {
codeResult = SourceEdit.applySequence(codeResult, edit.edits);
}
expect(codeResult, expectedCode.code);
// Check selection/position.
expect(snippet.change.selection!.file, testFile.path);
expect(snippet.change.selection!.offset, expectedCode.position.offset);
// And linked edits.
var expectedLinkedGroups =
expectedCode.ranges
.map(
(range) => {
'positions': [
{'file': testFile.path, 'offset': range.sourceRange.offset},
],
'length': range.sourceRange.length,
'suggestions': [],
},
)
.toSet();
var actualLinkedGroups =
snippet.change.linkedEditGroups.map((group) => group.toJson()).toSet();
expect(actualLinkedGroups, equals(expectedLinkedGroups));
}
Future<void> expectNotValidSnippet(String content) async {
var code = TestCode.parse(content);
await resolveTestCode(code.code);
var request = DartSnippetRequest(
unit: testAnalysisResult,
offset: code.position.offset,
);
var producer = generator(request, elementImportCache: {});
expect(await producer.isValid(), isFalse);
}
Future<Snippet> expectValidSnippet(TestCode code) async {
await resolveTestCode(code.code);
var request = DartSnippetRequest(
unit: testAnalysisResult,
offset: code.position.offset,
);
var producer = generator(request, elementImportCache: {});
expect(await producer.isValid(), isTrue);
return producer.compute();
}
}
abstract class FlutterSnippetProducerTest extends DartSnippetProducerTest {
/// Asserts that [change] matches the code in [expected], has a selection
/// matching its range and a single linked edit group containing all of its
/// positions.
void assertFlutterSnippetChange(
SourceChange change,
String linkedGroupText,
TestCode expected,
) {
expect(change.edits, hasLength(1));
var code = SourceEdit.applySequence('', change.edits.single.edits);
expect(code, expected.code);
expect(change.selection!.file, testFile.path);
expect(change.selection!.offset, expected.range.sourceRange.offset);
expect(change.selectionLength, expected.range.sourceRange.length);
expect(change.linkedEditGroups.map((group) => group.toJson()), [
{
'positions': [
for (final position in expected.positions)
{'file': testFile.path, 'offset': position.offset},
],
'length': linkedGroupText.length,
'suggestions': [],
},
]);
}
/// Checks snippets can produce edits where the imports and snippet will be
/// inserted at the same location.
///
/// For example, when a document is completely empty besides the snippet
/// prefix, the imports will be inserted at offset 0 and the snippet will
/// replace from 0 to the end of the typed prefix.
Future<void> test_valid_importsAndEditsOverlap() async {
writeTestPackageConfig(flutter: true);
var snippet = await expectValidSnippet(TestCode.parse('$prefix^'));
expect(snippet.prefix, prefix);
expect(snippet.label, label);
// Main edits replace $prefix.length characters starting at $prefix
var mainEdit = snippet.change.edits[0].edits[0];
expect(mainEdit.offset, testCode.indexOf(prefix));
expect(mainEdit.length, prefix.length);
// Imports inserted at start of doc (0)
var importEdit = snippet.change.edits[0].edits[1];
expect(importEdit.offset, 0);
expect(importEdit.length, 0);
}
Future<void> test_valid_suffixReplacement() async {
writeTestPackageConfig(flutter: true);
var snippet = await expectValidSnippet(
TestCode.parse('''
class A {}
$prefix^
'''),
);
expect(snippet.prefix, prefix);
expect(snippet.label, label);
// Main edits replace $prefix.length characters starting at $prefix
var mainEdit = snippet.change.edits[0].edits[0];
expect(mainEdit.offset, testCode.indexOf(prefix));
expect(mainEdit.length, prefix.length);
// Imports inserted at start of doc (0)
var importEdit = snippet.change.edits[0].edits[1];
expect(importEdit.offset, 0);
expect(importEdit.length, 0);
}
}