blob: d719d7aef9f1278ba1440d191267e39a9baaa68b [file] [log] [blame]
// Copyright (c) 2019, 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.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:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(FoldingTest);
});
}
@reflectiveTest
class FoldingTest extends AbstractLspAnalysisServerTest {
/// A placeholder for folding range kinds that are unset to make it clearer
/// what's being expected in tests.
static const FoldingRangeKind? noFoldingKind = null;
late TestCode code;
List<FoldingRange> ranges = [];
bool lineFoldingOnly = false;
Future<void> computeRanges(String sourceContent,
{Uri? uri, void Function()? initializePlugin}) async {
uri ??= mainFileUri;
code = TestCode.parse(sourceContent);
if (lineFoldingOnly) {
setLineFoldingOnly();
}
await initialize();
await openFile(uri, code.code);
initializePlugin?.call();
ranges = await getFoldingRanges(uri);
}
void expectNoRanges() {
expect(ranges, isEmpty);
}
void expectRanges(Map<int, FoldingRangeKind?> expected,
{bool requireAll = true}) {
var expectedRanges = expected.entries.map((entry) {
var range = code.ranges[entry.key].range;
return FoldingRange(
startLine: range.start.line,
startCharacter: lineFoldingOnly ? null : range.start.character,
endLine: range.end.line,
endCharacter: lineFoldingOnly ? null : range.end.character,
kind: entry.value,
);
}).toSet();
if (requireAll) {
expect(ranges, expectedRanges);
} else {
expect(ranges, containsAll(expectedRanges));
}
}
void expectRangesContain(Map<int, FoldingRangeKind?> expected) =>
expectRanges(expected, requireAll: false);
Future<void> test_class() async {
var content = '''
class MyClass2/*[0*/ {
// Class content
}/*0]*/
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
});
}
Future<void> test_comments() async {
var content = '''
/// This is a comment[/*[0*/
/// that spans many lines/*0]*/
class MyClass2 {}
''';
await computeRanges(content);
expectRanges({
0: FoldingRangeKind.Comment,
});
}
Future<void> test_doLoop() async {
var content = '''
f/*[0*/(int i) {
do {/*[1*/
print('with statements');/*1]*/
} while (i == 0);
do {/*[2*/
// only comments/*2]*/
} while (i == 0);
// empty
do {
} while (i == 0);
// no body
while (false);
}/*0]*/
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_enum() async {
var content = '''
enum MyEnum {/*[0*/
one,
two,
three
/*0]*/}
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
});
}
Future<void> test_forLoop() async {
var content = '''
void f() {
for (int i = 0; i < 1; i++) /*[0*/{
;
}/*0]*/
for (int i = 0; i < 1; i++) {}
for (int i = 0; i < 1; i++);
}
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
});
}
Future<void> test_fromPlugins_dartFile() async {
var pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.dart');
var pluginAnalyzedUri = pathContext.toUri(pluginAnalyzedFilePath);
const content = '''
// /*[0*/contributed by fake plugin/*0]*/
class AnnotatedDartClass/*[1*/ {
// content of dart class, contributed by server
}/*1]*/
''';
var pluginResult = plugin.AnalysisFoldingParams(
pluginAnalyzedFilePath,
[plugin.FoldingRegion(plugin.FoldingKind.DIRECTIVES, 3, 26)],
);
await computeRanges(
content,
uri: pluginAnalyzedUri,
initializePlugin: () =>
configureTestPlugin(notification: pluginResult.toNotification()),
);
expectRanges({
0: FoldingRangeKind.Imports, // From plugin
1: noFoldingKind, // From server
});
}
Future<void> test_fromPlugins_nonDartFile() async {
var pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.sql');
var pluginAnalyzedUri = pathContext.toUri(pluginAnalyzedFilePath);
const content = '''
CREATE TABLE foo(
/*[0*/-- some columns/*0]*/
);
''';
var pluginResult = plugin.AnalysisFoldingParams(
pluginAnalyzedFilePath,
[plugin.FoldingRegion(plugin.FoldingKind.CLASS_BODY, 22, 15)],
);
await computeRanges(
content,
uri: pluginAnalyzedUri,
initializePlugin: () =>
configureTestPlugin(notification: pluginResult.toNotification()),
);
expectRanges({
0: noFoldingKind, // From plugin
});
}
Future<void> test_functionExpression() async {
var content = '''
var x = () /*[0*/{
;
;
;
}/*0]*/;
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
});
}
Future<void> test_headersImportsComments() async {
var content = '''
// Copyright some year by some people/*[0*/
// See LICENCE etc./*0]*/
import/*[1*/ 'dart:io';
import 'dart:async';/*1]*/
/// This is not the file header/*[2*/
/// It's just a comment/*2]*/
void f() {}
''';
await computeRanges(content);
expectRanges({
0: FoldingRangeKind.Comment,
1: FoldingRangeKind.Imports,
2: FoldingRangeKind.Comment,
});
}
Future<void> test_ifElseElseIf() async {
var content = '''
f(int i) {
if (i == 0) {/*[0*/
// only
// comments/*0]*/
} else if (i == 1) {/*[1*/
print('statements');/*1]*/
} else if (i == 2) {
} else {/*[2*/
// else
// comments/*2]*/
}
}
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_multilineStrings() async {
var content = '''
var x = /*[0*/"""
1
2
3
"""/*0]*/;
var rx = /*[1*/r"""
1
2
3
"""/*1]*/;
var ix = /*[2*/"""
1
\$x
3
"""/*2]*/;
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_multilineStrings_lineFoldingOnly() async {
lineFoldingOnly = true;
var content = '''
var x = """/*[0*/
1
2
3
/*0]*/""";
var rx = r"""/*[1*/
1
2
3
/*1]*/""";
var ix = """/*[2*/
1
\$x
3
/*2]*/""";
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_nested() async {
var content = '''
class MyClass2/*[0*/ {
void f/*[1*/() {
void g/*[2*/() {
//
}/*2]*/
}/*1]*/
}/*0]*/
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_nested_lineFoldingOnly() async {
lineFoldingOnly = true;
var content = '''
class MyClass2 {/*[0*/
void f() {/*[1*/
void g() {/*[2*/
//
/*2]*/}
/*1]*/}
/*0]*/}
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
});
}
Future<void> test_nonDartFile() async {
await computeRanges(simplePubspecContent, uri: pubspecFileUri);
expectNoRanges();
}
/// When the client supports columns (not "lineFoldingOnly"), we can end
/// one range on the same line as the next one starts.
Future<void> test_overlapLines_columnsSupported() async {
var content = '''
void f/*[0*/() {
//
}/*0]*/ void g/*[1*/() {
//
}/*1]*/
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
});
}
/// When the client supports lineFoldingOnly, we cannot end a range on the
/// same line that the next one starts. Instead, it should be shortened to end
/// on the previous line.
Future<void> test_overlapLines_lineFoldingOnly() async {
lineFoldingOnly = true;
var content = '''
void f/*[0*/() {
///*0]*/
} void g/*[1*/() {
//
}/*1]*/
''';
await computeRanges(content);
expectRanges({
0: noFoldingKind,
1: noFoldingKind,
});
}
Future<void> test_recordLiteral() async {
var content = '''
void f() {
var r = (/*[0*/
2,
'string',
/*0]*/);
}
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
});
}
Future<void> test_switchExpression() async {
var content = '''
void f(int a) {
var b = switch (a) {/*[0*/
1 => '',
2 =>/*[1*/ '1234567890'
'1234567890'/*1]*/,
_ =>/*[2*/ '1234567890'
'1234567890'
'1234567890'
'1234567890'/*2]*/
}/*0]*/;
}
''';
await computeRanges(content);
expectRanges(
{
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
},
requireAll: false,
);
}
Future<void> test_switchPattern() async {
var content = '''
void f(int a) {
switch (a) {/*[0*/
case 0:/*[1*/
print('');/*1]*/
case 1:
case 2:/*[2*/
print('');/*2]*/
default:/*[3*/
print('');/*3]*/
}/*0]*/
}
''';
await computeRanges(content);
expectRanges(
{
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
3: noFoldingKind,
},
requireAll: false,
);
}
Future<void> test_switchStatement() async {
failTestOnErrorDiagnostic = false; // Tests cases without breaks.
var content = '''
// @dart = 2.19
void f(int a) {
switch (a) {/*[0*/
case 0:/*[1*/
print('');
break;/*1]*/
case 1:
case 2:/*[2*/
print('');/*2]*/
default:/*[3*/
print('');/*3]*/
}/*0]*/
}
''';
await computeRanges(content);
expectRanges(
{
0: noFoldingKind,
1: noFoldingKind,
2: noFoldingKind,
3: noFoldingKind,
},
requireAll: false,
);
}
Future<void> test_whileLoop() async {
var content = '''
f(int i) {
while (i == 0) {/*[0*/
print('with statements');/*0]*/
}
while (i == 0) {/*[1*/
// only comments/*1]*/
}
// empty
while (i == 0) {
}
// no body
while (i == 0);
}
''';
await computeRanges(content);
expectRangesContain({
0: noFoldingKind,
1: noFoldingKind,
});
}
}