Skip to content

Commit 2d09853

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Add explicit test for generating minimal edits
This is a minor refactor that adds an explicit test for generating minimal edits without going through the formatter. There's no change in behaviour - this is just to reduce the noise in a future CL for inserted/deleted commas to be supported in computing minimal diffs. Change-Id: I3d254bd7a90ccc7b11700b54110ece040e837363 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329601 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent c0c9e96 commit 2d09853

File tree

8 files changed

+152
-37
lines changed

8 files changed

+152
-37
lines changed

pkg/analysis_server/lib/src/lsp/source_edits.dart

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import 'package:analyzer/src/generated/source.dart';
1616
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
1717
import 'package:dart_style/dart_style.dart';
1818

19-
DartFormatter formatter = DartFormatter();
19+
DartFormatter _formatter = DartFormatter();
2020

2121
/// Transforms a sequence of LSP document change events to a sequence of source
2222
/// edits used by analysis plugins.
@@ -84,10 +84,10 @@ ErrorOr<List<TextEdit>?> generateEditsForFormatting(
8484
SourceCode formattedResult;
8585
try {
8686
// If the lineLength has changed, recreate the formatter with the new setting.
87-
if (lineLength != formatter.pageWidth) {
88-
formatter = DartFormatter(pageWidth: lineLength);
87+
if (lineLength != _formatter.pageWidth) {
88+
_formatter = DartFormatter(pageWidth: lineLength);
8989
}
90-
formattedResult = formatter.formatSource(code);
90+
formattedResult = _formatter.formatSource(code);
9191
} on FormatterException {
9292
// If the document fails to parse, just return no edits to avoid the
9393
// use seeing edits on every save with invalid code (if LSP gains the
@@ -101,10 +101,10 @@ ErrorOr<List<TextEdit>?> generateEditsForFormatting(
101101
return success(null);
102102
}
103103

104-
return _generateMinimalEdits(result, formattedSource, range: range);
104+
return generateMinimalEdits(result, formattedSource, range: range);
105105
}
106106

107-
List<TextEdit> _generateFullEdit(
107+
List<TextEdit> generateFullEdit(
108108
LineInfo lineInfo, String unformattedSource, String formattedSource) {
109109
final end = lineInfo.getLocation(unformattedSource.length);
110110
return [
@@ -116,15 +116,15 @@ List<TextEdit> _generateFullEdit(
116116
];
117117
}
118118

119-
/// Generates edits that modify the minimum amount of code (only whitespace) to
120-
/// change [unformatted] to [formatted].
119+
/// Generates edits that modify the minimum amount of code (if only whitespace,
120+
/// commas and comments) to change the source of [result] to [formatted].
121121
///
122122
/// This allows editors to more easily track important locations (such as
123123
/// breakpoints) without needing to do their own diffing.
124124
///
125-
/// If [range] is supplied, only whitespace edits that fall entirely inside this
126-
/// range will be included in the results.
127-
ErrorOr<List<TextEdit>> _generateMinimalEdits(
125+
/// If [range] is supplied, only edits that fall entirely inside this range will
126+
/// be included in the results.
127+
ErrorOr<List<TextEdit>> generateMinimalEdits(
128128
ParsedUnitResult result,
129129
String formatted, {
130130
Range? range,
@@ -146,7 +146,7 @@ ErrorOr<List<TextEdit>> _generateMinimalEdits(
146146
final parsedFormatted = _parse(formatted, result.unit.featureSet);
147147
final parsedUnformatted = _parse(unformatted, result.unit.featureSet);
148148
if (parsedFormatted == null || parsedUnformatted == null) {
149-
return success(_generateFullEdit(lineInfo, unformatted, formatted));
149+
return success(generateFullEdit(lineInfo, unformatted, formatted));
150150
}
151151

152152
final unformattedTokens = _iterateAllTokens(parsedUnformatted).iterator;
@@ -270,7 +270,7 @@ ErrorOr<List<TextEdit>> _generateMinimalEdits(
270270
// If the token lexemes do not match, there is a difference in the parsed
271271
// token streams (this should not ordinarily happen) so fall back to a
272272
// full edit.
273-
return success(_generateFullEdit(lineInfo, unformatted, formatted));
273+
return success(generateFullEdit(lineInfo, unformatted, formatted));
274274
}
275275

276276
addEditFor(
@@ -293,7 +293,7 @@ ErrorOr<List<TextEdit>> _generateMinimalEdits(
293293
// If we got here and either of the streams still have tokens, something
294294
// did not match so fall back to a full edit.
295295
if (unformattedHasMore || formattedHasMore) {
296-
return success(_generateFullEdit(lineInfo, unformatted, formatted));
296+
return success(generateFullEdit(lineInfo, unformatted, formatted));
297297
}
298298

299299
// Finally, handle any whitespace that was after the last token.

pkg/analysis_server/test/integration/lsp/handle_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ void main() {
3232
/// functionality are in `test/lsp`.
3333
@reflectiveTest
3434
class LspOverLegacyTest extends AbstractAnalysisServerIntegrationTest
35-
with LspRequestHelpersMixin {
35+
with LspRequestHelpersMixin, LspEditHelpersMixin {
3636
late final testFile = sourcePath('lib/test.dart');
3737

3838
Uri get testFileUri => Uri.file(testFile);

pkg/analysis_server/test/integration/lsp_server/integration_tests.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ abstract class AbstractLspAnalysisServerIntegrationTest
2121
with
2222
ClientCapabilitiesHelperMixin,
2323
LspRequestHelpersMixin,
24+
LspEditHelpersMixin,
2425
LspAnalysisServerTestMixin {
2526
final List<String> vmArgs = [];
2627
LspServerClient? client;

pkg/analysis_server/test/lsp/request_helpers_mixin.dart

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,7 @@ import 'package:test/test.dart' hide expect;
1515

1616
import 'change_verifier.dart';
1717

18-
/// Helpers to simplify building LSP requests for use in tests.
19-
///
20-
/// The actual sending of requests must be supplied by the implementing class
21-
/// via [expectSuccessfulResponseTo].
22-
///
23-
/// These helpers can be used by in-process tests and out-of-process integration
24-
/// tests and by both the native LSP server and using LSP over the legacy
25-
/// protocol.
26-
mixin LspRequestHelpersMixin {
27-
int _id = 0;
28-
29-
final startOfDocPos = Position(line: 0, character: 0);
30-
31-
final startOfDocRange = Range(
32-
start: Position(line: 0, character: 0),
33-
end: Position(line: 0, character: 0));
34-
35-
/// Whether to include 'clientRequestTime' fields in outgoing messages.
36-
bool includeClientRequestTime = false;
37-
18+
mixin LspEditHelpersMixin {
3819
String applyTextEdit(String content, TextEdit edit) {
3920
final startPos = edit.range.start;
4021
final endPos = edit.range.end;
@@ -93,6 +74,27 @@ mixin LspRequestHelpersMixin {
9374
indexedEdits.sort(TextEditWithIndex.compare);
9475
return indexedEdits.map((e) => e.edit).fold(content, applyTextEdit);
9576
}
77+
}
78+
79+
/// Helpers to simplify building LSP requests for use in tests.
80+
///
81+
/// The actual sending of requests must be supplied by the implementing class
82+
/// via [expectSuccessfulResponseTo].
83+
///
84+
/// These helpers can be used by in-process tests and out-of-process integration
85+
/// tests and by both the native LSP server and using LSP over the legacy
86+
/// protocol.
87+
mixin LspRequestHelpersMixin {
88+
int _id = 0;
89+
90+
final startOfDocPos = Position(line: 0, character: 0);
91+
92+
final startOfDocRange = Range(
93+
start: Position(line: 0, character: 0),
94+
end: Position(line: 0, character: 0));
95+
96+
/// Whether to include 'clientRequestTime' fields in outgoing messages.
97+
bool includeClientRequestTime = false;
9698

9799
Future<List<CallHierarchyIncomingCall>?> callHierarchyIncoming(
98100
CallHierarchyItem item) {

pkg/analysis_server/test/lsp/server_abstract.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ abstract class AbstractLspAnalysisServerTest
4545
ResourceProviderMixin,
4646
ClientCapabilitiesHelperMixin,
4747
LspRequestHelpersMixin,
48+
LspEditHelpersMixin,
4849
LspAnalysisServerTestMixin,
4950
ConfigurationFilesMixin {
5051
late MockLspServerChannel channel;
@@ -807,7 +808,8 @@ mixin ConfigurationFilesMixin on ResourceProviderMixin {
807808
}
808809
}
809810

810-
mixin LspAnalysisServerTestMixin on LspRequestHelpersMixin
811+
mixin LspAnalysisServerTestMixin
812+
on LspRequestHelpersMixin, LspEditHelpersMixin
811813
implements ClientCapabilitiesHelperMixin {
812814
static const positionMarker = '^';
813815
static const rangeMarkerStart = '[[';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:analysis_server/lsp_protocol/protocol.dart';
8+
import 'package:analysis_server/src/lsp/source_edits.dart';
9+
import 'package:test/test.dart';
10+
import 'package:test_reflective_loader/test_reflective_loader.dart';
11+
12+
import '../abstract_single_unit.dart';
13+
import 'request_helpers_mixin.dart';
14+
15+
void main() {
16+
defineReflectiveSuite(() {
17+
defineReflectiveTests(SourceEditsTest);
18+
});
19+
}
20+
21+
@reflectiveTest
22+
class SourceEditsTest extends AbstractSingleUnitTest with LspEditHelpersMixin {
23+
Future<void> test_minimalEdits() async {
24+
const startContent = '''
25+
void f(){}
26+
''';
27+
const endContent = '''
28+
void f() {
29+
}
30+
''';
31+
const expectedEdits = r'''
32+
Delete 1:6-1:8
33+
Insert " " at 1:11
34+
Insert "\n" at 1:12
35+
''';
36+
37+
await _assertMinimalEdits(startContent, endContent, expectedEdits);
38+
}
39+
40+
/// Assert that computing minimal edits to convert [start] to [end] produces
41+
/// the set of edits described in [expected].
42+
///
43+
/// Edits will be automatically applied and verified. [expected] is to ensure
44+
/// the edits are minimal and we didn't accidentally produces a single edit
45+
/// replacing the entire file.
46+
Future<void> _assertMinimalEdits(
47+
String start,
48+
String end,
49+
String expected,
50+
) async {
51+
start = start.trim();
52+
end = end.trim();
53+
expected = expected.trim();
54+
await parseTestCode(start);
55+
final edits = generateMinimalEdits(testParsedResult, end);
56+
expect(edits.toText().trim(), expected);
57+
expect(applyTextEdits(start, edits.result), end);
58+
}
59+
}
60+
61+
/// Helpers for building simple text representations of edits to verify that
62+
/// minimal diffs were produced.
63+
///
64+
/// Does not include actual content - resulting content should be verified
65+
/// separately.
66+
extension on List<TextEdit> {
67+
String toText() => map((edit) => edit.toText()).join('\n');
68+
}
69+
70+
/// Helpers for building simple text representations of edits to verify that
71+
/// minimal diffs were produced.
72+
///
73+
/// Does not include actual content - resulting content should be verified
74+
/// separately.
75+
extension on TextEdit {
76+
String toText() {
77+
return range.start == range.end
78+
? 'Insert ${jsonEncode(newText)} at ${range.start.toText()}'
79+
: newText.isEmpty
80+
? 'Delete ${range.toText()}'
81+
: 'Replace ${range.toText()} with ${jsonEncode(newText)}';
82+
}
83+
}
84+
85+
/// Helpers for building simple text representations of edits to verify that
86+
/// minimal diffs were produced.
87+
extension on Range {
88+
String toText() => '${start.toText()}-${end.toText()}';
89+
}
90+
91+
/// Helpers for building simple text representations of edits to verify that
92+
/// minimal diffs were produced.
93+
extension on Position {
94+
String toText() => '${line + 1}:${character + 1}';
95+
}
96+
97+
/// Helpers for building simple text representations of edits to verify that
98+
/// minimal diffs were produced.
99+
///
100+
/// Does not include actual content - resulting content should be verified
101+
/// separately.
102+
extension on ErrorOr<List<TextEdit>> {
103+
String toText() => map(
104+
(error) => 'Error: ${error.message}',
105+
(result) => result.toText(),
106+
);
107+
}

pkg/analysis_server/test/lsp/test_all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import 'semantic_tokens_test.dart' as semantic_tokens;
4545
import 'server_test.dart' as server;
4646
import 'signature_help_test.dart' as signature_help;
4747
import 'snippets_test.dart' as snippets;
48+
import 'source_edits_test.dart' as source_edits;
4849
import 'super_test.dart' as get_super;
4950
import 'temporary_overlay_operation_test.dart' as temporary_overlay_operation;
5051
import 'type_definition_test.dart' as type_definition;
@@ -96,6 +97,7 @@ void main() {
9697
server.main();
9798
signature_help.main();
9899
snippets.main();
100+
source_edits.main();
99101
temporary_overlay_operation.main();
100102
type_definition.main();
101103
type_hierarchy.main();

pkg/analysis_server/test/lsp_over_legacy/format_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:test/test.dart';
66
import 'package:test_reflective_loader/test_reflective_loader.dart';
77

8+
import '../lsp/request_helpers_mixin.dart';
89
import 'abstract_lsp_over_legacy.dart';
910

1011
void main() {
@@ -14,7 +15,7 @@ void main() {
1415
}
1516

1617
@reflectiveTest
17-
class FormatTest extends LspOverLegacyTest {
18+
class FormatTest extends LspOverLegacyTest with LspEditHelpersMixin {
1819
Future<void> test_format() async {
1920
const content = 'void main() {}';
2021
const expectedContent = 'void main() {}';

0 commit comments

Comments
 (0)