Skip to content

Commit e885432

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] [lsp] Support navigation from return/break/continue/yield to loop/function
This adds navigation support via Go-to-Definition for LSP for these keywords, matching similar behaviour in other languages (like TypeScript) in VS Code. See #50532 Change-Id: I30601b54c8ed7235b7789582032b9565618f2148 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/441520 Commit-Queue: Samuel Rawlins <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]>
1 parent c235fad commit e885432

File tree

3 files changed

+315
-3
lines changed

3 files changed

+315
-3
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:analysis_server/src/lsp/mapping.dart';
1111
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
1212
import 'package:analysis_server/src/plugin/result_merger.dart';
1313
import 'package:analysis_server/src/protocol_server.dart' show NavigationTarget;
14+
import 'package:analysis_server/src/utilities/navigation/keyword_navigation_computer.dart';
1415
import 'package:analyzer/dart/analysis/results.dart';
1516
import 'package:analyzer/dart/ast/ast.dart';
1617
import 'package:analyzer/dart/element/element.dart';
@@ -77,6 +78,18 @@ class DefinitionHandler
7778
offset,
7879
0,
7980
);
81+
82+
// If there are no results, then try keyword navigation.
83+
if (collector.regions.isEmpty) {
84+
var keywordNavigationComputer = KeywordNavigationComputer(
85+
collector,
86+
result.libraryFragment,
87+
);
88+
keywordNavigationComputer.compute(
89+
result.unit.nodeCovering(offset: offset),
90+
);
91+
}
92+
8093
if (supportsLocationLink) {
8194
await _updateTargetsWithCodeLocations(collector);
8295
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) 2025, 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 'package:analyzer/dart/ast/token.dart';
6+
import 'package:analyzer/dart/element/element.dart';
7+
import 'package:analyzer/src/dart/ast/ast.dart';
8+
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
9+
import 'package:analyzer_plugin/utilities/navigation/navigation.dart';
10+
11+
/// Computes navigation targets for keywords like `break`, `continue` and
12+
/// `return`.
13+
class KeywordNavigationComputer {
14+
final NavigationCollector collector;
15+
final LibraryFragment libraryFrament;
16+
17+
KeywordNavigationComputer(this.collector, this.libraryFrament);
18+
19+
void compute(AstNode? node) {
20+
if (node is! Statement) return;
21+
22+
var target = switch (node) {
23+
BreakStatement() ||
24+
ContinueStatement() => _findBreakOrContinueTarget(node),
25+
ReturnStatement() || YieldStatement() => _findReturnOrYieldTarget(node),
26+
_ => null,
27+
};
28+
29+
if (target != null) {
30+
_addRegion(node.beginToken, target);
31+
}
32+
}
33+
34+
void _addRegion(Token sourceToken, Token targetToken) {
35+
var targetStart = libraryFrament.lineInfo.getLocation(targetToken.offset);
36+
collector.addRegion(
37+
sourceToken.offset,
38+
sourceToken.length,
39+
protocol.ElementKind.UNKNOWN,
40+
protocol.Location(
41+
libraryFrament.source.fullName,
42+
targetToken.offset,
43+
targetToken.length,
44+
targetStart.lineNumber,
45+
targetStart.columnNumber,
46+
),
47+
);
48+
}
49+
50+
Token? _findBreakOrContinueTarget(Statement statement) {
51+
return switch (statement) {
52+
BreakStatement() => statement.target?.beginToken,
53+
ContinueStatement() => statement.target?.beginToken,
54+
_ => null,
55+
};
56+
}
57+
58+
Token? _findReturnOrYieldTarget(Statement statement) {
59+
// Find the enclosing function, constructor or method.
60+
var function = statement.thisOrAncestorOfType<FunctionBody>()?.parent;
61+
return switch (function) {
62+
FunctionExpression(:FunctionDeclaration parent) => parent.name,
63+
// No name for closures, so just use the first token (the opening paren).
64+
FunctionExpression() => function.beginToken,
65+
MethodDeclaration() => function.name,
66+
ConstructorDeclaration() =>
67+
// For named constructors, return the name.
68+
// For unnamed constructors, use the return type / class name.
69+
function.name ?? function.returnType.beginToken,
70+
_ => null,
71+
};
72+
}
73+
}

pkg/analysis_server/test/lsp/definition_test.dart

Lines changed: 229 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ class A {
451451
var contents = '''
452452
class A {
453453
int [!_!] = 1;
454-
int f() => _^;
454+
int f() => _^;
455455
}
456456
''';
457457

@@ -661,6 +661,217 @@ math.Random? r;
661661
}
662662
}
663663

664+
Future<void> test_keywordNavigation_break_toDo() async {
665+
var contents = '''
666+
void f() {
667+
[!do!] {
668+
if (true) br^eak;
669+
} while (true);
670+
}
671+
''';
672+
673+
await testContents(contents);
674+
}
675+
676+
Future<void> test_keywordNavigation_break_toFor() async {
677+
var contents = '''
678+
void f() {
679+
[!for!] (;;) {
680+
if (true) br^eak;
681+
}
682+
}
683+
''';
684+
685+
await testContents(contents);
686+
}
687+
688+
Future<void> test_keywordNavigation_break_toSwitch() async {
689+
var contents = '''
690+
void f(int x) {
691+
[!switch!] (x) {
692+
case 1:
693+
br^eak;
694+
}
695+
}
696+
''';
697+
698+
await testContents(contents);
699+
}
700+
701+
Future<void> test_keywordNavigation_break_toWhile() async {
702+
var contents = '''
703+
void f() {
704+
[!while!] (true) {
705+
if (true) br^eak;
706+
}
707+
}
708+
''';
709+
710+
await testContents(contents);
711+
}
712+
713+
Future<void> test_keywordNavigation_break_withLabel() async {
714+
var contents = '''
715+
void f() {
716+
outer:
717+
[!do!] {
718+
do {
719+
if (true) br^eak outer;
720+
} while (true);
721+
} while (true);
722+
}
723+
''';
724+
725+
await testContents(contents);
726+
}
727+
728+
Future<void> test_keywordNavigation_continue_toFor() async {
729+
var contents = '''
730+
void f() {
731+
[!for!] (;;) {
732+
if (true) cont^inue;
733+
}
734+
}
735+
''';
736+
737+
await testContents(contents);
738+
}
739+
740+
Future<void> test_keywordNavigation_continue_toFor_insideSwitch() async {
741+
var contents = '''
742+
void f() {
743+
[!for!] (;;) {
744+
switch (true) {
745+
case _: cont^inue;
746+
}
747+
}
748+
}
749+
''';
750+
751+
await testContents(contents);
752+
}
753+
754+
Future<void> test_keywordNavigation_continue_toWhile() async {
755+
var contents = '''
756+
void f() {
757+
[!while!] (true) {
758+
if (true) cont^inue;
759+
}
760+
}
761+
''';
762+
763+
await testContents(contents);
764+
}
765+
766+
Future<void> test_keywordNavigation_nestedLoop() async {
767+
var contents = '''
768+
void f() {
769+
for (;;) {
770+
[!while!] (true) {
771+
if (true) br^eak;
772+
}
773+
}
774+
}
775+
''';
776+
777+
await testContents(contents);
778+
}
779+
780+
Future<void> test_keywordNavigation_noTarget() async {
781+
failTestOnErrorDiagnostic = false;
782+
783+
var contents = '''
784+
void f() {
785+
br^eak; // no loop
786+
}
787+
''';
788+
await testContents(contents, expectNoResults: true);
789+
}
790+
791+
Future<void> test_keywordNavigation_return_toClosure() async {
792+
var contents = '''
793+
int foo() {
794+
return [1].firstWhere([!(!]i) {
795+
ret^urn true;
796+
});
797+
}
798+
''';
799+
800+
await testContents(contents);
801+
}
802+
803+
Future<void> test_keywordNavigation_return_toConstructor_named() async {
804+
var contents = '''
805+
class MyClass {
806+
MyClass.[!fooConstructor!]() {
807+
ret^urn;
808+
}
809+
}
810+
''';
811+
812+
await testContents(contents);
813+
}
814+
815+
Future<void> test_keywordNavigation_return_toConstructor_unnamed() async {
816+
var contents = '''
817+
class MyClass {
818+
[!MyClass!]() {
819+
ret^urn;
820+
}
821+
}
822+
''';
823+
824+
await testContents(contents);
825+
}
826+
827+
Future<void> test_keywordNavigation_return_toFunction() async {
828+
var contents = '''
829+
int [!foo!]() {
830+
if (true) ret^urn 42;
831+
return 0;
832+
}
833+
''';
834+
835+
await testContents(contents);
836+
}
837+
838+
Future<void> test_keywordNavigation_return_toGetter() async {
839+
var contents = '''
840+
class C {
841+
int get [!value!] {
842+
if (true) ret^urn 42;
843+
return 0;
844+
}
845+
}
846+
''';
847+
848+
await testContents(contents);
849+
}
850+
851+
Future<void> test_keywordNavigation_yield_toFunction() async {
852+
var contents = '''
853+
Iterable<int> [!generator!]() sync* {
854+
yi^eld 1;
855+
yield 2;
856+
}
857+
''';
858+
859+
await testContents(contents);
860+
}
861+
862+
Future<void> test_label() async {
863+
var contents = '''
864+
f() {
865+
[!lbl!]:
866+
for (;;) {
867+
break lb^l;
868+
}
869+
}
870+
''';
871+
872+
await testContents(contents);
873+
}
874+
664875
Future<void> test_locationLink_class() async {
665876
setLocationLinkSupport();
666877

@@ -889,7 +1100,7 @@ void f() {
8891100
var contents = '''
8901101
class A {
8911102
int [!_!]() => 1;
892-
int f() => _^();
1103+
int f() => _^();
8931104
}
8941105
''';
8951106

@@ -1150,7 +1361,11 @@ class [!MyClass!] {}
11501361

11511362
/// Expects definitions at the location of `^` in [contents] will navigate to
11521363
/// the range in `[!` brackets `!]` in `[contents].
1153-
Future<void> testContents(String contents, {bool inOpenFile = true}) async {
1364+
Future<void> testContents(
1365+
String contents, {
1366+
bool inOpenFile = true,
1367+
bool expectNoResults = false,
1368+
}) async {
11541369
var code = TestCode.parse(contents);
11551370
await initialize();
11561371
if (inOpenFile) {
@@ -1161,6 +1376,17 @@ class [!MyClass!] {}
11611376
code.position.position,
11621377
);
11631378

1379+
if (expectNoResults) {
1380+
expect(
1381+
code.ranges,
1382+
isEmpty,
1383+
reason: 'TestCode should not contain ranges if expectNoResults=true',
1384+
);
1385+
expect(res, isEmpty);
1386+
return;
1387+
}
1388+
1389+
expect(code.ranges, hasLength(1));
11641390
expect(res, hasLength(1));
11651391
var loc = res.single;
11661392
expect(loc.range, equals(code.range.range));

0 commit comments

Comments
 (0)