Skip to content

Commit 732305f

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[Analyzer] Don't show duplicate LSP code actions on same line
Fixes Dart-Code/Dart-Code#2960. Change-Id: I63361728e80c7db843db9e2b31e76eae78f85400 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/172562 Commit-Queue: Danny Tuppeny <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent 3a8a9fe commit 732305f

File tree

2 files changed

+103
-22
lines changed

2 files changed

+103
-22
lines changed

pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import 'package:analysis_server/src/lsp/constants.dart';
1212
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
1313
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
1414
import 'package:analysis_server/src/lsp/mapping.dart';
15-
import 'package:analysis_server/src/protocol_server.dart';
15+
import 'package:analysis_server/src/protocol_server.dart' hide Position;
1616
import 'package:analysis_server/src/services/correction/assist.dart';
1717
import 'package:analysis_server/src/services/correction/assist_internal.dart';
1818
import 'package:analysis_server/src/services/correction/bulk_fix_processor.dart';
@@ -92,6 +92,21 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
9292
});
9393
}
9494

95+
/// Creates a comparer for [CodeActions] that compares the column distance from [pos].
96+
Function(CodeAction a, CodeAction b) _codeActionColumnDistanceComparer(
97+
Position pos) {
98+
Position posOf(CodeAction action) => action.diagnostics.isNotEmpty
99+
? action.diagnostics.first.range.start
100+
: pos;
101+
102+
return (a, b) => _columnDistance(posOf(a), pos)
103+
.compareTo(_columnDistance(posOf(b), pos));
104+
}
105+
106+
/// Returns the distance (in columns, ignoring lines) between two positions.
107+
int _columnDistance(Position a, Position b) =>
108+
(a.character - b.character).abs();
109+
95110
/// Wraps a command in a CodeAction if the client supports it so that a
96111
/// CodeActionKind can be supplied.
97112
Either2<Command, CodeAction> _commandOrCodeAction(
@@ -152,33 +167,60 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
152167
);
153168
}
154169

155-
/// Dedupes actions that perform the same edit and merge their diagnostics
156-
/// together. This avoids duplicates where there are multiple errors on
157-
/// the same line that have the same fix (for example importing a
158-
/// library that fixes multiple unresolved types).
159-
List<CodeAction> _dedupeActions(Iterable<CodeAction> actions) {
160-
final groups = groupBy(actions, (CodeAction action) => action.edit);
161-
return groups.keys.map((edit) {
162-
final first = groups[edit].first;
163-
// Avoid constructing new CodeActions if there was only one in this group.
164-
if (groups[edit].length == 1) {
165-
return first;
170+
/// Dedupes/merges actions that have the same title, selecting the one nearest [pos].
171+
///
172+
/// If actions perform the same edit/command, their diagnostics will be merged
173+
/// together. Otherwise, the additional accounts are just dropped.
174+
///
175+
/// The first diagnostic for an action is used to determine the position (using
176+
/// its `start`). If there is no diagnostic, it will be treated as being at [pos].
177+
///
178+
/// If multiple actions have the same position, one will arbitrarily be chosen.
179+
List<CodeAction> _dedupeActions(Iterable<CodeAction> actions, Position pos) {
180+
final groups = groupBy(actions, (CodeAction action) => action.title);
181+
return groups.keys.map((title) {
182+
final actions = groups[title];
183+
184+
// If there's only one in the group, just return it.
185+
if (actions.length == 1) {
186+
return actions.single;
166187
}
188+
189+
// Otherwise, find the action nearest to the caret.
190+
actions.sort(_codeActionColumnDistanceComparer(pos));
191+
final first = actions.first;
192+
193+
// Get any actions with the same fix (edit/command) for merging diagnostics.
194+
final others = actions.skip(1).where(
195+
(other) =>
196+
// Compare either edits or commands based on which the selected action has.
197+
first.edit != null
198+
? first.edit == other.edit
199+
: first.command != null
200+
? first.command == other.command
201+
: false,
202+
);
203+
167204
// Build a new CodeAction that merges the diagnostics from each same
168205
// code action onto a single one.
169206
return CodeAction(
170-
title: first.title,
171-
kind: first.kind,
172-
// Merge diagnostics from all of the CodeActions.
173-
diagnostics: groups[edit].expand((r) => r.diagnostics).toList(),
174-
edit: first.edit,
175-
command: first.command);
207+
title: first.title,
208+
kind: first.kind,
209+
// Merge diagnostics from all of the matching CodeActions.
210+
diagnostics: [
211+
...?first.diagnostics,
212+
for (final other in others) ...?other.diagnostics,
213+
],
214+
edit: first.edit,
215+
command: first.command,
216+
);
176217
}).toList();
177218
}
178219

179220
Future<List<Either2<Command, CodeAction>>> _getAssistActions(
180221
HashSet<CodeActionKind> clientSupportedCodeActionKinds,
181222
bool clientSupportsLiteralCodeActions,
223+
Range range,
182224
int offset,
183225
int length,
184226
ResolvedUnitResult unit,
@@ -201,7 +243,8 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
201243
final assists = await processor.compute();
202244
assists.sort(Assist.SORT_BY_RELEVANCE);
203245

204-
final assistActions = _dedupeActions(assists.map(_createAssistAction));
246+
final assistActions =
247+
_dedupeActions(assists.map(_createAssistAction), range.start);
205248

206249
return assistActions
207250
.map((action) => Either2<Command, CodeAction>.t2(action))
@@ -228,7 +271,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
228271
final results = await Future.wait([
229272
_getSourceActions(
230273
kinds, supportsLiterals, supportsWorkspaceApplyEdit, path),
231-
_getAssistActions(kinds, supportsLiterals, offset, length, unit),
274+
_getAssistActions(kinds, supportsLiterals, range, offset, length, unit),
232275
_getRefactorActions(kinds, supportsLiterals, path, offset, length, unit),
233276
_getFixActions(
234277
kinds, supportsLiterals, supportedDiagnosticTags, range, unit),
@@ -328,7 +371,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
328371
// Append all fix-alls to the very end.
329372
codeActions.addAll(fixAllCodeActions);
330373

331-
final dedupedActions = _dedupeActions(codeActions);
374+
final dedupedActions = _dedupeActions(codeActions, range.start);
332375

333376
return dedupedActions
334377
.map((action) => Either2<Command, CodeAction>.t2(action))

pkg/analysis_server/test/lsp/code_actions_fixes_test.dart

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class FixesCodeActionsTest extends AbstractCodeActionsTest {
9696
expect(contents[mainFilePath], equals(expectedContent));
9797
}
9898

99-
Future<void> test_noDuplicates() async {
99+
Future<void> test_noDuplicates_sameFix() async {
100100
const content = '''
101101
var a = [Test, Test, Te[[]]st];
102102
''';
@@ -321,4 +321,42 @@ void f(String a) {
321321
await verifyCodeActionEdits(
322322
fixAction, withoutMarkers(content), expectedContent);
323323
}
324+
325+
Future<void> test_noDuplicates_differentFix() async {
326+
// For convenience, quick-fixes are usually returned for the entire line,
327+
// though this can lead to duplicate entries (by title) when multiple
328+
// diagnostics have their own fixes of the same type.
329+
//
330+
// Expect only the only one nearest to the start of the range to be returned.
331+
const content = '''
332+
main() {
333+
var a = [];
334+
print(a!!);^
335+
}
336+
''';
337+
338+
newFile(mainFilePath, content: withoutMarkers(content));
339+
await initialize(
340+
textDocumentCapabilities: withCodeActionKinds(
341+
emptyTextDocumentClientCapabilities, [CodeActionKind.QuickFix]),
342+
);
343+
344+
final caretPos = positionFromMarker(content);
345+
346+
final codeActions = await getCodeActions(mainFileUri.toString(),
347+
range: Range(start: caretPos, end: caretPos));
348+
final removeNnaAction = findEditActions(codeActions,
349+
CodeActionKind('quickfix.remove.nonNullAssertion'), "Remove the '!'");
350+
351+
// Expect only one of the fixes.
352+
expect(removeNnaAction, hasLength(1));
353+
354+
// Ensure the action is for the diagnostic on the second bang which was
355+
// closest to the range requested.
356+
final secondBangPos =
357+
positionFromOffset(withoutMarkers(content).indexOf('!);'), content);
358+
expect(removeNnaAction.first.diagnostics, hasLength(1));
359+
final diagStart = removeNnaAction.first.diagnostics.first.range.start;
360+
expect(diagStart, equals(secondBangPos));
361+
}
324362
}

0 commit comments

Comments
 (0)