59587b0e27
+ fix several failing tests with this enabled. See https://github.com/dart-lang/sdk/issues/60234 Change-Id: Ie71e1655bf2ab8ac651bfcf803884741863b42c1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423801 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
728 lines
18 KiB
Dart
728 lines
18 KiB
Dart
// 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 'dart:io';
|
|
|
|
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:analyzer/src/test_utilities/test_code_format.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_format_version_defaultsToLatest() async {
|
|
// Latest version should parse and format records.
|
|
const startContent = '''
|
|
var a = (1, 2);
|
|
''';
|
|
const endContent = '''
|
|
var a = (1, 2);
|
|
''';
|
|
const expectedEdits = r'''
|
|
Delete 1:5-1:8
|
|
''';
|
|
|
|
await _assertFormatEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_format_version_languageVersionToken() async {
|
|
// 2.19 will not parse/format records.
|
|
const content = '''
|
|
// @dart = 2.19
|
|
var a = (1, 2);
|
|
''';
|
|
|
|
await _assertNoFormatEdits(content);
|
|
}
|
|
|
|
Future<void> test_format_version_packageConfig() async {
|
|
// 2.19 will not parse/format records.
|
|
writeTestPackageConfig(languageVersion: '2.19');
|
|
const content = '''
|
|
var a = (1, 2);
|
|
''';
|
|
|
|
await _assertNoFormatEdits(content);
|
|
}
|
|
|
|
Future<void> test_format_version_versionToken_overridesPackageConfig() async {
|
|
// 2.19 will not parse/format records.
|
|
writeTestPackageConfig(languageVersion: '3.0');
|
|
const content = '''
|
|
// @dart = 2.19
|
|
var a = (1, 2);
|
|
''';
|
|
|
|
await _assertNoFormatEdits(content);
|
|
}
|
|
|
|
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_commaAndSemicolon_remove() async {
|
|
const startContent = '''
|
|
enum SomeEnum { a, b, c,; }
|
|
''';
|
|
const endContent = '''
|
|
enum SomeEnum { a, b, c }
|
|
''';
|
|
const expectedEdits = r'''
|
|
Delete 1:24-1:26
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
/// The formatter removes trailing whitespace from comments which results in
|
|
/// differences in the comment token lexemes. This should
|
|
/// be handled and not result in a full document edit.
|
|
///
|
|
/// https://github.com/Dart-Code/Dart-Code/issues/5200
|
|
Future<void> test_minimalEdits_comment_multiLine_trailingWhitespace() async {
|
|
// The initial content has a trailing space on the end of the comment.
|
|
var startContent =
|
|
TestCode.parse(r'''
|
|
/**
|
|
* line with trailing whitespace /**/
|
|
* line with trailing whitespace /**/
|
|
*/
|
|
int? a;
|
|
''').code;
|
|
// We expect the trailing spaces to be removed.
|
|
const endContent = r'''
|
|
/**
|
|
* line with trailing whitespace
|
|
* line with trailing whitespace
|
|
*/
|
|
int? a;
|
|
''';
|
|
|
|
// Expect the edit to replace the entire comment, minus the common
|
|
// prefix/suffix. That is, it will replace from the end of the first line
|
|
// of the comment (consuming the trailing whitespace) up up until the
|
|
// trailing space of the second line.
|
|
// We do not support minimizing the edits within the comment itself, because
|
|
// we only diff tokens and not the string contents.
|
|
const expectedEdits = r'''
|
|
Replace 2:33-3:34 with "\n * line with trailing whitespace"
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
/// The formatter removes trailing whitespace from comments which results in
|
|
/// differences in the comment token lexemes. This should
|
|
/// be handled and not result in a full document edit.
|
|
///
|
|
/// https://github.com/Dart-Code/Dart-Code/issues/5200
|
|
Future<void> test_minimalEdits_comment_singleLine_trailingWhitespace() async {
|
|
// The initial content has a trailing space on the end of the comment.
|
|
const startContent = 'const a = 1; // a \nconst b = 2;';
|
|
// We expect the trailing space will be removed.
|
|
const endContent = 'const a = 1; // a\nconst b = 2;';
|
|
|
|
// Expect the edit to be only the deletion of that one character, not a
|
|
// full edit (or full replacement of the comment).
|
|
const expectedEdits = r'''
|
|
Delete 1:18-1:19
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
/// Empty collections that are unwrapped produce different tokens. This should
|
|
/// be handled and not result in a full document edit.
|
|
///
|
|
/// https://github.com/Dart-Code/Dart-Code/issues/5169
|
|
Future<void> test_minimalEdits_emptyCollection() async {
|
|
const startContent = '''
|
|
var a = <String>[
|
|
];
|
|
var b = '';
|
|
''';
|
|
const endContent = '''
|
|
var a = <String>[];
|
|
var b = '';
|
|
''';
|
|
// Expect the newline to be deleted.
|
|
const expectedEdits = r'''
|
|
Delete 1:18-2:1
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_formatting_shortStyle() async {
|
|
const startContent = '''
|
|
// @dart = 3.5
|
|
|
|
void f({String argument1, String argument2}) {}
|
|
|
|
void g() {
|
|
f(argument1: 'An argument', argument2: 'Another argument');
|
|
}
|
|
''';
|
|
const endContent = '''
|
|
// @dart = 3.5
|
|
|
|
void f({String argument1, String argument2}) {}
|
|
|
|
void g() {
|
|
f(argument1: 'An argument', argument2: 'Another argument');
|
|
}
|
|
''';
|
|
const expectedEdits = '';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
@FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/56685')
|
|
Future<void> test_minimalEdits_formatting_tallStyle() async {
|
|
const startContent = '''
|
|
void f({String? argument1, String? argument2}) {}
|
|
|
|
void g() {
|
|
f(argument1: 'An argument', argument2: 'Another argument');
|
|
}
|
|
''';
|
|
const endContent = '''
|
|
void f({String? argument1, String? argument2}) {}
|
|
|
|
void g() {
|
|
f(argument1: 'An argument',
|
|
argument2: 'Another argument',
|
|
);
|
|
}
|
|
''';
|
|
const expectedEdits = r'''
|
|
Insert "\n " at 4:31
|
|
Insert ",\n" at 4:60
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_gt_2_combined() async {
|
|
const startContent = '''
|
|
List<
|
|
List<String>
|
|
> a = [];
|
|
''';
|
|
const endContent = '''
|
|
List<List<String>> a = [];
|
|
''';
|
|
const expectedEdits = r'''
|
|
Delete 1:6-2:3
|
|
Delete 2:15-3:1
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_gt_2_split() async {
|
|
const startContent = '''
|
|
List<List<String>> a = [];
|
|
''';
|
|
const endContent = '''
|
|
List<
|
|
List<String>
|
|
> a = [];
|
|
''';
|
|
const expectedEdits = r'''
|
|
Insert "\n " at 1:6
|
|
Insert "\n" at 1:18
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_gt_3_combined() async {
|
|
const startContent = '''
|
|
List<
|
|
List<
|
|
List<String>
|
|
>
|
|
> a = [];
|
|
''';
|
|
const endContent = '''
|
|
List<List<List<String>>> a = [];
|
|
''';
|
|
const expectedEdits = r'''
|
|
Delete 1:6-2:3
|
|
Delete 2:8-3:5
|
|
Replace 3:17-5:1 with ">"
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_gt_3_split() async {
|
|
const startContent = '''
|
|
List<List<List<String>>> a = [];
|
|
''';
|
|
const endContent = '''
|
|
List<
|
|
List<
|
|
List<String>
|
|
>
|
|
> a = [];
|
|
''';
|
|
const expectedEdits = r'''
|
|
Insert "\n " at 1:6
|
|
Insert "\n " at 1:11
|
|
Replace 1:23-1:24 with "\n >\n"
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
Future<void> test_minimalEdits_semicolon_remove() async {
|
|
const startContent = '''
|
|
enum SomeEnum {
|
|
a,
|
|
b,
|
|
c;
|
|
}
|
|
''';
|
|
const endContent = '''
|
|
enum SomeEnum {
|
|
a,
|
|
b,
|
|
c
|
|
}
|
|
''';
|
|
const expectedEdits = r'''
|
|
Delete 4:4-4:5
|
|
''';
|
|
|
|
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);
|
|
}
|
|
|
|
/// If we fail to compute minimal edits but were formatting the entire doc,
|
|
/// we should just return the entire edit.
|
|
Future<void> test_minimalEdits_withoutRange_wholeDocument() async {
|
|
const startContent = '''
|
|
void f() {
|
|
}
|
|
''';
|
|
// The simulated formatted content is different (`g` instead of `f`) which
|
|
// will fail to produce minimal edits (since it varies by more than
|
|
// whitespace).
|
|
const endContent = '''
|
|
void g() {
|
|
}
|
|
''';
|
|
const expectedEdits = r'''
|
|
Replace 1:1-2:2 with "void g() {\n}"
|
|
''';
|
|
|
|
await _assertMinimalEdits(startContent, endContent, expectedEdits);
|
|
}
|
|
|
|
/// If we fail to compute minimal edits but were formatting a range,
|
|
/// we should return no edits (rather than the entire document).
|
|
///
|
|
/// https://github.com/Dart-Code/Dart-Code/issues/5169
|
|
Future<void> test_minimalEdits_withRange_emptyResult() async {
|
|
const startContent = '''
|
|
void f() {
|
|
}
|
|
''';
|
|
// The simulated formatted content is different (`g` instead of `f`) which
|
|
// will fail to produce minimal edits (since it varies by more than
|
|
// whitespace).
|
|
const endContent = '''
|
|
void g() {
|
|
}
|
|
''';
|
|
const expectedEdits = r''; // No edits.
|
|
|
|
await _assertMinimalEdits(
|
|
startContent,
|
|
endContent,
|
|
expectedEdits,
|
|
range: Range(
|
|
start: Position(line: 1, character: 1),
|
|
end: Position(line: 1, character: 1),
|
|
),
|
|
// We should end with the original content because there was no
|
|
// formatting.
|
|
expectedFormatResult: startContent,
|
|
);
|
|
}
|
|
|
|
/// Assert that generating edits to format [start] match those described
|
|
/// in [expected] and when applied, result in [end].
|
|
///
|
|
/// 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> _assertFormatEdits(
|
|
String start,
|
|
String end,
|
|
String expected, {
|
|
String? expectedFormatResult,
|
|
Range? range,
|
|
}) async {
|
|
start = normalizeSource(start);
|
|
end = normalizeSource(end);
|
|
|
|
await parseTestCode(start);
|
|
var edits =
|
|
generateEditsForFormatting(testParsedResult, range: range).result!;
|
|
expect(edits.toText().trim(), expected.trim());
|
|
expect(applyTextEdits(start, edits), expectedFormatResult ?? end);
|
|
}
|
|
|
|
/// 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, {
|
|
String? expectedFormatResult,
|
|
Range? range,
|
|
}) async {
|
|
start = normalizeSource(start.trim());
|
|
end = normalizeSource(end.trim());
|
|
if (expectedFormatResult != null) {
|
|
expectedFormatResult = normalizeSource(expectedFormatResult.trim());
|
|
}
|
|
expected = expected.trim();
|
|
|
|
// Expectation descriptions are always written with literal newlines as '\n'
|
|
// so on Windows we will need to update the expectation to '\r\n' before
|
|
// comparing.
|
|
expect(
|
|
expected,
|
|
isNot(contains(r'\r')),
|
|
reason:
|
|
'Expectations should always be written with literal '
|
|
r"'\n' to indicate newlines regardless of platform",
|
|
);
|
|
if (Platform.isWindows) {
|
|
expected = expected.replaceAll(r'\n', r'\r\n');
|
|
}
|
|
|
|
await parseTestCode(start);
|
|
var edits = generateMinimalEdits(testParsedResult, end, range: range);
|
|
expect(edits.toText().trim(), expected);
|
|
expect(applyTextEdits(start, edits.result), expectedFormatResult ?? end);
|
|
}
|
|
|
|
/// Assert that formatting [content] produces no edits.
|
|
Future<void> _assertNoFormatEdits(String content) async {
|
|
await parseTestCode(content);
|
|
var edits = generateEditsForFormatting(testParsedResult).result;
|
|
expect(edits, isNull);
|
|
}
|
|
}
|
|
|
|
/// 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());
|
|
}
|