Skip to content

Commit 02ea7d0

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] [lsp] Support return/yield/break/continue in Document Highlights
This adds support for keywords like `return`, `yield`, `break`, `continue` to Document Highlights. For break/continue, the matching loop keyword (`do`/`while`/`for`) is also highlighted (and this works in both directions). This behaviour matches what I see for TypeScript in VS Code. This is only supported for LSP because the legacy protocol groups these by "Elements" which we don't have for loops (though since IntelliJ uses its own data for occurrences, it's not clear to me if anyone is using Occurrences over the legacy protocol). Fixes #61170 Change-Id: I5563ec0a91a6fe33d10a6317d33e9256b3c50209 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/442061 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]>
1 parent 7da04d0 commit 02ea7d0

File tree

6 files changed

+456
-90
lines changed

6 files changed

+456
-90
lines changed

pkg/analysis_server/lib/src/domains/analysis/occurrences_dart.dart

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import 'package:analyzer/dart/ast/token.dart';
99
import 'package:analyzer/dart/ast/visitor.dart';
1010
import 'package:analyzer/dart/element/element.dart';
1111
import 'package:analyzer/src/dart/ast/extensions.dart';
12+
import 'package:analyzer/src/utilities/extensions/collection.dart';
1213

1314
void addDartOccurrences(OccurrencesCollector collector, CompilationUnit unit) {
1415
var visitor = DartUnitOccurrencesComputerVisitor();
1516
unit.accept(visitor);
16-
visitor.elementsOffsetLengths.forEach((engineElement, offsetLengths) {
17+
visitor.elementOccurrences.forEach((engineElement, nodes) {
1718
// For legacy protocol, we only support occurrences with the same
1819
// length, so we must filter the offset to only those that match the length
1920
// from the element.
@@ -24,18 +25,51 @@ void addDartOccurrences(OccurrencesCollector collector, CompilationUnit unit) {
2425
var length =
2526
serverElement.location?.length ?? engineElement.name?.length ?? 0;
2627
var offsets =
27-
offsetLengths
28-
.where((offsetLength) => offsetLength.$2 == length)
29-
.map((offsetLength) => offsetLength.$1)
28+
nodes
29+
.where((node) => node.length == length)
30+
.map((node) => node.offset)
3031
.toList();
3132

3233
var occurrences = protocol.Occurrences(serverElement, offsets, length);
3334
collector.addOccurrences(occurrences);
3435
});
3536
}
3637

37-
class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
38-
final Map<Element, List<(int, int)>> elementsOffsetLengths = {};
38+
/// Returns both element-based and node-based occurrences for a file.
39+
///
40+
/// The legacy protocol does not support occurrences that are not based on
41+
/// elements and therefore use [addDartOccurrences]. This method is for LSP
42+
/// which does not use elements and instead only cares about the resulting
43+
/// ranges of code, allowing ranges for keywords like `return` and `break` to be
44+
/// included.
45+
List<Occurrences> getAllOccurrences(CompilationUnit unit) {
46+
// TODO(dantup): Since LSP always starts with a target element or node,
47+
// we should consider passing it in here to avoid building the occurrences
48+
// for the whole file and then extracting only the matches we want.
49+
var visitor = DartUnitOccurrencesComputerVisitor();
50+
unit.accept(visitor);
51+
52+
return [
53+
// Element-based occurrences
54+
for (var MapEntry(key: element, value: tokens)
55+
in visitor.elementOccurrences.entries)
56+
ElementOccurrences(element, tokens),
57+
// Node-based occurrences
58+
for (var MapEntry(key: node, value: tokens)
59+
in visitor.nodeOccurrences.entries)
60+
NodeOccurrences(node, tokens),
61+
];
62+
}
63+
64+
class DartUnitOccurrencesComputerVisitor extends GeneralizingAstVisitor<void> {
65+
/// Occurrences tracked by their elements.
66+
final Map<Element, List<Token>> elementOccurrences = {};
67+
68+
/// Occurrences tracked by nodes (such as loops and their exit keywords).
69+
final Map<AstNode, List<Token>> nodeOccurrences = {};
70+
71+
// Stack to track the current function for return/yield keywords
72+
final List<AstNode> _functionStack = [];
3973

4074
@override
4175
void visitAssignedVariablePattern(AssignedVariablePattern node) {
@@ -47,6 +81,13 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
4781
super.visitAssignedVariablePattern(node);
4882
}
4983

84+
@override
85+
void visitBreakStatement(BreakStatement node) {
86+
_addNodeOccurrence(node.target, node.breakKeyword);
87+
88+
super.visitBreakStatement(node);
89+
}
90+
5091
@override
5192
void visitClassDeclaration(ClassDeclaration node) {
5293
_addOccurrence(node.declaredFragment!.element, node.name);
@@ -66,10 +107,9 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
66107
if (node.name case var name?) {
67108
_addOccurrence(node.declaredFragment!.element, name);
68109
} else {
69-
_addOccurrenceAt(
110+
_addOccurrence(
70111
node.declaredFragment!.element,
71-
node.returnType.offset,
72-
node.returnType.length,
112+
node.returnType.beginToken,
73113
);
74114
}
75115

@@ -91,6 +131,13 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
91131
super.visitConstructorName(node);
92132
}
93133

134+
@override
135+
void visitContinueStatement(ContinueStatement node) {
136+
_addNodeOccurrence(node.target, node.continueKeyword);
137+
138+
super.visitContinueStatement(node);
139+
}
140+
94141
@override
95142
void visitDeclaredIdentifier(DeclaredIdentifier node) {
96143
_addOccurrence(node.declaredFragment!.element, node.name);
@@ -109,6 +156,13 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
109156
super.visitDeclaredVariablePattern(node);
110157
}
111158

159+
@override
160+
void visitDoStatement(DoStatement node) {
161+
_addNodeOccurrence(node, node.doKeyword);
162+
163+
super.visitDoStatement(node);
164+
}
165+
112166
@override
113167
void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
114168
_addOccurrence(node.declaredFragment!.element, node.name);
@@ -159,6 +213,20 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
159213
super.visitFieldFormalParameter(node);
160214
}
161215

216+
@override
217+
void visitForStatement(ForStatement node) {
218+
_addNodeOccurrence(node, node.forKeyword);
219+
220+
super.visitForStatement(node);
221+
}
222+
223+
@override
224+
void visitFunctionBody(FunctionBody node) {
225+
_functionStack.add(node);
226+
super.visitFunctionBody(node);
227+
_functionStack.removeLastOrNull();
228+
}
229+
162230
@override
163231
void visitFunctionDeclaration(FunctionDeclaration node) {
164232
_addOccurrence(node.declaredFragment!.element, node.name);
@@ -235,6 +303,13 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
235303
super.visitRepresentationDeclaration(node);
236304
}
237305

306+
@override
307+
void visitReturnStatement(ReturnStatement node) {
308+
_addNodeOccurrence(_functionStack.lastOrNull, node.returnKeyword);
309+
310+
super.visitReturnStatement(node);
311+
}
312+
238313
@override
239314
void visitSimpleFormalParameter(SimpleFormalParameter node) {
240315
var nameToken = node.name;
@@ -270,6 +345,13 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
270345
super.visitSuperFormalParameter(node);
271346
}
272347

348+
@override
349+
void visitSwitchStatement(SwitchStatement node) {
350+
_addNodeOccurrence(node, node.switchKeyword);
351+
352+
super.visitSwitchStatement(node);
353+
}
354+
273355
@override
274356
void visitTypeParameter(TypeParameter node) {
275357
if (node case TypeParameter(:var declaredFragment?)) {
@@ -285,21 +367,32 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
285367
super.visitVariableDeclaration(node);
286368
}
287369

288-
void _addOccurrence(Element element, Token token) {
289-
_addOccurrenceAt(element, token.offset, token.length);
370+
@override
371+
void visitWhileStatement(WhileStatement node) {
372+
_addNodeOccurrence(node, node.whileKeyword);
373+
374+
super.visitWhileStatement(node);
375+
}
376+
377+
@override
378+
void visitYieldStatement(YieldStatement node) {
379+
_addNodeOccurrence(_functionStack.lastOrNull, node.yieldKeyword);
380+
381+
super.visitYieldStatement(node);
382+
}
383+
384+
void _addNodeOccurrence(AstNode? node, Token token) {
385+
if (node == null) return;
386+
387+
(nodeOccurrences[node] ??= []).add(token);
290388
}
291389

292-
void _addOccurrenceAt(Element element, int offset, int length) {
390+
void _addOccurrence(Element element, Token token) {
293391
var canonicalElement = _canonicalizeElement(element);
294392
if (canonicalElement == null) {
295393
return;
296394
}
297-
var offsetLengths = elementsOffsetLengths[canonicalElement];
298-
if (offsetLengths == null) {
299-
offsetLengths = <(int, int)>[];
300-
elementsOffsetLengths[canonicalElement] = offsetLengths;
301-
}
302-
offsetLengths.add((offset, length));
395+
(elementOccurrences[canonicalElement] ??= []).add(token);
303396
}
304397

305398
Element? _canonicalizeElement(Element element) {
@@ -314,3 +407,28 @@ class DartUnitOccurrencesComputerVisitor extends RecursiveAstVisitor<void> {
314407
return canonicalElement?.baseElement;
315408
}
316409
}
410+
411+
/// Occurrences grouped by an Element.
412+
class ElementOccurrences extends Occurrences {
413+
final Element element;
414+
415+
@override
416+
final List<Token> tokens;
417+
418+
ElementOccurrences(this.element, this.tokens);
419+
}
420+
421+
/// Occurrences grouped by a node (for example exit keywords grouped by a loop).
422+
class NodeOccurrences extends Occurrences {
423+
final AstNode node;
424+
425+
@override
426+
final List<Token> tokens;
427+
428+
NodeOccurrences(this.node, this.tokens);
429+
}
430+
431+
/// Base class for protocol-agnostic occurrences.
432+
sealed class Occurrences {
433+
List<Token> get tokens;
434+
}

pkg/analysis_server/lib/src/legacy_analysis_server.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import 'package:analysis_server/src/analytics/analytics_manager.dart';
1919
import 'package:analysis_server/src/channel/channel.dart';
2020
import 'package:analysis_server/src/computer/computer_highlights.dart';
2121
import 'package:analysis_server/src/domains/analysis/occurrences.dart';
22-
import 'package:analysis_server/src/domains/analysis/occurrences_dart.dart';
22+
import 'package:analysis_server/src/domains/analysis/occurrences_dart.dart'
23+
hide Occurrences;
2324
import 'package:analysis_server/src/flutter/flutter_notifications.dart';
2425
import 'package:analysis_server/src/handler/legacy/analysis_get_errors.dart';
2526
import 'package:analysis_server/src/handler/legacy/analysis_get_hover.dart';

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

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:analysis_server/src/lsp/error_or.dart';
88
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
99
import 'package:analysis_server/src/lsp/mapping.dart';
1010
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
11-
import 'package:collection/collection.dart';
11+
import 'package:analyzer/dart/ast/token.dart';
1212

1313
typedef StaticOptions = Either2<bool, DocumentHighlightOptions>;
1414

@@ -45,42 +45,37 @@ class DocumentHighlightsHandler
4545
var offset = unit.mapResultSync((unit) => toOffset(unit.lineInfo, pos));
4646

4747
return (unit, offset).mapResults((unit, requestedOffset) async {
48-
var visitor = DartUnitOccurrencesComputerVisitor();
49-
unit.unit.accept(visitor);
48+
var occurrences = getAllOccurrences(unit.unit);
5049

51-
/// Checks whether an Occurrence offset/length spans the requested
50+
/// Checks whether an Occurrence token spans the requested
5251
/// offset.
5352
///
5453
/// It's possible multiple occurrences might match because some nodes
5554
/// such as object destructuring might match multiple elements (for
5655
/// example the object getter and a declared variable).
57-
bool spansRequestedPosition((int, int) offsetLength) {
58-
var (offset, length) = offsetLength;
59-
return offset <= requestedOffset && offset + length >= requestedOffset;
56+
bool spansRequestedPosition(Token token) {
57+
return token.offset <= requestedOffset && token.end >= requestedOffset;
6058
}
6159

62-
// Find the groups (elements) of offset/lengths that contains an
60+
// Find the groups of tokens that contains an
6361
// offset/length that spans the requested range. There may be multiple
6462
// matches here if the source element is in multiple groups.
65-
var matchingSet =
66-
visitor.elementsOffsetLengths.values
67-
.where(
68-
(offsetLengths) => offsetLengths.any(spansRequestedPosition),
69-
)
70-
.flattened
71-
.toSet();
63+
var matchingSet = <Token>{};
64+
65+
for (var occurrence in occurrences) {
66+
var tokens = occurrence.tokens;
67+
if (tokens.any(spansRequestedPosition)) {
68+
matchingSet.addAll(tokens);
69+
}
70+
}
7271

7372
// No matches will return an empty list (not null) because that prevents
7473
// the editor falling back to a text search.
7574
var highlights =
7675
matchingSet
7776
.map(
78-
(offsetLength) => DocumentHighlight(
79-
range: toRange(
80-
unit.lineInfo,
81-
offsetLength.$1,
82-
offsetLength.$2,
83-
),
77+
(token) => DocumentHighlight(
78+
range: toRange(unit.lineInfo, token.offset, token.length),
8479
),
8580
)
8681
.toList();

pkg/analysis_server/lib/src/services/refactoring/legacy/rename_parameter.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import 'package:analyzer_plugin/protocol/protocol_common.dart';
1616
/// A [Refactoring] for renaming [analyzer.FormalParameterElement]s.
1717
class RenameParameterRefactoringImpl extends RenameRefactoringImpl {
1818
List<analyzer.FormalParameterElement> elements = [];
19-
bool _renameAllPositionalOccurences = false;
19+
bool _renameAllPositionalOccurrences = false;
2020

2121
RenameParameterRefactoringImpl(
2222
super.workspace,
@@ -51,8 +51,8 @@ class RenameParameterRefactoringImpl extends RenameRefactoringImpl {
5151
if (resolvedUnit != null) {
5252
// If any of the resolved units have the lint enabled, we should avoid
5353
// renaming method parameters separately from the other implementations.
54-
if (element.isPositional && !_renameAllPositionalOccurences) {
55-
_renameAllPositionalOccurences |=
54+
if (element.isPositional && !_renameAllPositionalOccurrences) {
55+
_renameAllPositionalOccurrences |=
5656
getCodeStyleOptions(
5757
resolvedUnit.file,
5858
).avoidRenamingMethodParameters;
@@ -69,7 +69,7 @@ class RenameParameterRefactoringImpl extends RenameRefactoringImpl {
6969
);
7070
}
7171
}
72-
if (_renameAllPositionalOccurences && elements.length > 1) {
72+
if (_renameAllPositionalOccurrences && elements.length > 1) {
7373
result.addStatus(
7474
_RefactoringStatusExt.from(
7575
'This will also rename all related positional parameters '
@@ -97,7 +97,7 @@ class RenameParameterRefactoringImpl extends RenameRefactoringImpl {
9797
for (var element in elements) {
9898
if (element != this.element &&
9999
element.isPositional &&
100-
!_renameAllPositionalOccurences) {
100+
!_renameAllPositionalOccurrences) {
101101
continue;
102102
}
103103
var fieldRenamed = false;

0 commit comments

Comments
 (0)