blob: 626777bc2f3a4470d71b75609b1c0ba9abfea31d [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/source_edits.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../abstract_single_unit.dart';
import 'request_helpers_mixin.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SourceEditsTest);
});
}
@reflectiveTest
class SourceEditsTest extends AbstractSingleUnitTest with LspEditHelpersMixin {
Future<void> test_minimalEdits_comma_delete() async {
const startContent = '''
void f(int a,) {}
''';
const endContent = '''
void f(int a) {}
''';
const expectedEdits = r'''
Delete 1:13-1:14
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_delete_afterBlockComment() async {
const startContent = '''
void f(int a /* before */,);
''';
const endContent = '''
void f(int a /* before */);
''';
const expectedEdits = r'''
Delete 1:26-1:27
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_delete_afterWhitespace() async {
const startContent = '''
void f(int a ,) {}
''';
const endContent = '''
void f(int a ) {}
''';
const expectedEdits = r'''
Delete 1:14-1:15
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_delete_beforeWhitespace() async {
const startContent = '''
void f(int a, ) {}
''';
const endContent = '''
void f(int a ) {}
''';
const expectedEdits = r'''
Delete 1:13-1:14
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_delete_betweenBlockComments() async {
const startContent = '''
void f(int a /* before */ , /* after */);
''';
const endContent = '''
void f(int a /* before */ /* after */);
''';
const expectedEdits = r'''
Delete 1:27-1:29
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void>
test_minimalEdits_comma_delete_betweenBlockComments_withWrapping() async {
const startContent = '''
void f(veryLongArgument, argument /* before */ , /* after */ argument);
''';
const endContent = '''
void f(
veryLongArgument,
argument, /* before */
/* after */ argument,
);
''';
const expectedEdits = r'''
Insert "\n " at 1:8
Insert "\n " at 1:25
Insert "," at 1:34
Replace 1:47-1:49 with "\n "
Insert ",\n" at 1:70
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_delete_betweenWhitespace() async {
const startContent = '''
void f(int a , ) {}
''';
const endContent = '''
void f(int a ) {}
''';
const expectedEdits = r'''
Delete 1:14-1:15
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insert() async {
const startContent = '''
void f(int a) {}
''';
const endContent = '''
void f(int a,) {}
''';
const expectedEdits = r'''
Insert "," at 1:13
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insert_afterWhitespace() async {
const startContent = '''
void f(int a ) {}
''';
const endContent = '''
void f(int a ,) {}
''';
const expectedEdits = r'''
Insert "," at 1:14
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insert_beforeWhitespace() async {
const startContent = '''
void f(
int a
) {}
''';
const endContent = '''
void f(
int a,
) {}
''';
const expectedEdits = r'''
Insert "," at 2:8
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insert_betweenWhitespace() async {
const startContent = '''
void f(int a ) {}
''';
const endContent = '''
void f(int a , ) {}
''';
const expectedEdits = r'''
Insert "," at 1:14
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void>
test_minimalEdits_comma_insertWithLeadingAndTrailingWhitespace() async {
const startContent = '''
void f(int a) {}
''';
const endContent = '''
void f(int a , ) {}
''';
const expectedEdits = r'''
Insert " , " at 1:13
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insertWithLeadingWhitespace() async {
const startContent = '''
void f(int a) {}
''';
const endContent = '''
void f(int a ,) {}
''';
const expectedEdits = r'''
Insert " ," at 1:13
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_insertWithTrailingWhitespace() async {
const startContent = '''
void f(int a) {}
''';
const endContent = '''
void f(int a, ) {}
''';
const expectedEdits = r'''
Insert ", " at 1:13
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_comma_move() async {
const startContent = '''
void f(
int a // comment
,) {
}
''';
const endContent = '''
void f(
int a, // comment
) {
}
''';
const expectedEdits = r'''
Insert "," at 2:8
Delete 3:1-3:2
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
Future<void> test_minimalEdits_whitespace() async {
const startContent = '''
void f(){}
''';
const endContent = '''
void f() {
}
''';
const expectedEdits = r'''
Delete 1:6-1:8
Insert " " at 1:11
Insert "\n" at 1:12
''';
await _assertMinimalEdits(startContent, endContent, expectedEdits);
}
/// Assert that computing minimal edits to convert [start] to [end] produces
/// the set of edits described in [expected].
///
/// Edits will be automatically applied and verified. [expected] is to ensure
/// the edits are minimal and we didn't accidentally produces a single edit
/// replacing the entire file.
Future<void> _assertMinimalEdits(
String start,
String end,
String expected,
) async {
start = start.trim();
end = end.trim();
expected = expected.trim();
await parseTestCode(start);
final edits = generateMinimalEdits(testParsedResult, end);
expect(edits.toText().trim(), expected);
expect(applyTextEdits(start, edits.result), end);
}
}
/// Helpers for building simple text representations of edits to verify that
/// minimal diffs were produced.
///
/// Does not include actual content - resulting content should be verified
/// separately.
extension on List<TextEdit> {
String toText() => map((edit) => edit.toText()).join('\n');
}
/// Helpers for building simple text representations of edits to verify that
/// minimal diffs were produced.
///
/// Does not include actual content - resulting content should be verified
/// separately.
extension on TextEdit {
String toText() {
return range.start == range.end
? 'Insert ${jsonEncode(newText)} at ${range.start.toText()}'
: newText.isEmpty
? 'Delete ${range.toText()}'
: 'Replace ${range.toText()} with ${jsonEncode(newText)}';
}
}
/// Helpers for building simple text representations of edits to verify that
/// minimal diffs were produced.
extension on Range {
String toText() => '${start.toText()}-${end.toText()}';
}
/// Helpers for building simple text representations of edits to verify that
/// minimal diffs were produced.
extension on Position {
String toText() => '${line + 1}:${character + 1}';
}
/// Helpers for building simple text representations of edits to verify that
/// minimal diffs were produced.
///
/// Does not include actual content - resulting content should be verified
/// separately.
extension on ErrorOr<List<TextEdit>> {
String toText() => map(
(error) => 'Error: ${error.message}',
(result) => result.toText(),
);
}