Skip to content

Commit 2cc6b61

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Support LSP inlineValues for variables and parameters
This adds support for LSP's "Inline Values" for variables and properties, allowing their values to be seen floating in the editor while stepping with a debugger without having to hover. Including property access/getters may be useful too, but that can be added in a future CL (and perhaps initially behind a flag to get feedback, in case it turns out to bee too noisy). Fixes #59891 Change-Id: I6305cfa9a7583c30500b90ee3b29852c82cc2494 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412080 Commit-Queue: Brian Wilkerson <[email protected]> Reviewed-by: Phil Quitslund <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent a10602b commit 2cc6b61

File tree

11 files changed

+454
-20
lines changed

11 files changed

+454
-20
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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:analysis_server/lsp_protocol/protocol.dart';
6+
7+
extension PositionExtension on Position {
8+
/// Check if this position is after or equal to [other].
9+
bool isAfterOrEqual(Position other) =>
10+
line > other.line || (line == other.line && character >= other.character);
11+
12+
/// Check if this position is before or equal to [other].
13+
bool isBeforeOrEqual(Position other) =>
14+
line < other.line || (line == other.line && character <= other.character);
15+
}
16+
17+
extension RangeExtension on Range {
18+
/// Check if this range intersects with [other].
19+
bool intersects(Range other) {
20+
var endsBefore = end.isBeforeOrEqual(other.start);
21+
var startsAfter = start.isAfterOrEqual(other.end);
22+
return !(endsBefore || startsAfter);
23+
}
24+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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:analysis_server/lsp_protocol/protocol.dart' hide MessageType;
6+
import 'package:analysis_server/lsp_protocol/protocol.dart';
7+
import 'package:analysis_server/src/lsp/error_or.dart';
8+
import 'package:analysis_server/src/lsp/extensions/positions.dart';
9+
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
10+
import 'package:analysis_server/src/lsp/mapping.dart';
11+
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
12+
import 'package:analyzer/dart/ast/ast.dart';
13+
import 'package:analyzer/dart/ast/visitor.dart';
14+
import 'package:analyzer/dart/element/element2.dart';
15+
import 'package:analyzer/source/line_info.dart';
16+
import 'package:analyzer/src/dart/element/extensions.dart';
17+
18+
typedef StaticOptions =
19+
Either3<bool, InlineValueOptions, InlineValueRegistrationOptions>;
20+
21+
class InlineValueHandler
22+
extends
23+
SharedMessageHandler<InlineValueParams, TextDocumentInlineValueResult> {
24+
InlineValueHandler(super.server);
25+
26+
@override
27+
Method get handlesMessage => Method.textDocument_inlineValue;
28+
29+
@override
30+
LspJsonHandler<InlineValueParams> get jsonHandler =>
31+
InlineValueParams.jsonHandler;
32+
33+
@override
34+
bool get requiresTrustedCaller => false;
35+
36+
@override
37+
Future<ErrorOr<TextDocumentInlineValueResult>> handle(
38+
InlineValueParams params,
39+
MessageInfo message,
40+
CancellationToken token,
41+
) async {
42+
if (!isDartDocument(params.textDocument)) {
43+
return success(null);
44+
}
45+
46+
var filePath = pathOfDoc(params.textDocument);
47+
return filePath.mapResult((filePath) async {
48+
var unitResult = await server.getResolvedUnit(filePath);
49+
if (unitResult == null) {
50+
return success(null);
51+
}
52+
var lineInfo = unitResult.lineInfo;
53+
54+
// We will provide values from the start of the visible range up to
55+
// the end of the line the debugger is stopped on (which will do by just
56+
// jumping to position 0 of the next line).
57+
var visibleRange = params.range;
58+
var stoppedLocation = params.context.stoppedLocation;
59+
var applicableRange = Range(
60+
start: visibleRange.start,
61+
end: Position(line: stoppedLocation.end.line + 1, character: 0),
62+
);
63+
64+
var stoppedOffset = toOffset(lineInfo, stoppedLocation.end);
65+
return stoppedOffset.mapResult((stoppedOffset) async {
66+
// Find the function that is executing. We will only show values for
67+
// this single function expression.
68+
var node = await server.getNodeAtOffset(filePath, stoppedOffset);
69+
var function = node?.thisOrAncestorOfType<FunctionExpression>();
70+
if (function == null) {
71+
return success(null);
72+
}
73+
74+
var collector = _InlineValueCollector(lineInfo, applicableRange);
75+
var visitor = _InlineValueVisitor(collector, function);
76+
function.accept(visitor);
77+
78+
return success(collector.values.values.toList());
79+
});
80+
});
81+
}
82+
}
83+
84+
class InlineValueRegistrations extends FeatureRegistration
85+
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
86+
InlineValueRegistrations(super.info);
87+
88+
@override
89+
ToJsonable? get options =>
90+
TextDocumentRegistrationOptions(documentSelector: dartFiles);
91+
92+
@override
93+
Method get registrationMethod => Method.textDocument_inlineValue;
94+
95+
@override
96+
StaticOptions get staticOptions => Either3.t1(true);
97+
98+
@override
99+
bool get supportsDynamic => clientDynamic.inlineValue;
100+
}
101+
102+
/// Collects inline values, keeping only the most relevant where an element
103+
/// is recorded multiple times.
104+
class _InlineValueCollector {
105+
/// A map of elements and their inline value.
106+
final Map<Element2, InlineValue> values = {};
107+
108+
/// The range for which inline values should be retained.
109+
///
110+
/// This should be approximately the range of the visible code on screen up to
111+
/// the point of execution.
112+
final Range applicableRange;
113+
114+
/// A [LineInfo] used to convert offsets to lines/columns for comparing to
115+
/// [applicableRange].
116+
final LineInfo lineInfo;
117+
118+
_InlineValueCollector(this.lineInfo, this.applicableRange);
119+
120+
/// Records a variable inline value for [element] with [offset]/[length].
121+
///
122+
/// Variable inline values are sent to the client without expressions/names
123+
/// because the client can infer the name from the range and look it up from
124+
/// the debuggers Scopes/Variables.
125+
void recordVariableLookup(Element2? element, int offset, int length) {
126+
if (element == null || element.isWildcardVariable) return;
127+
128+
assert(offset >= 0);
129+
assert(length > 0);
130+
131+
var range = toRange(lineInfo, offset, length);
132+
133+
// Never record anything outside of the visible range.
134+
if (!range.intersects(applicableRange)) {
135+
return;
136+
}
137+
138+
// We only want to show each variable once, so keep only the one furthest
139+
// into the source (closest to the execution pointer).
140+
var existingPosition = values[element]?.map(
141+
(expression) => expression.range.start,
142+
(text) => text.range.start,
143+
(variable) => variable.range.start,
144+
);
145+
if (existingPosition != null &&
146+
existingPosition.isAfterOrEqual(range.start)) {
147+
return;
148+
}
149+
150+
values[element] = InlineValue.t3(
151+
InlineValueVariableLookup(
152+
caseSensitiveLookup: true,
153+
range: range,
154+
// We don't provide name, because it always matches the source code
155+
// for a variable and can be inferred.
156+
),
157+
);
158+
}
159+
}
160+
161+
/// Visits a function expression and reports nodes that should have inline
162+
/// values to [collector].
163+
class _InlineValueVisitor extends GeneralizingAstVisitor<void> {
164+
final _InlineValueCollector collector;
165+
final FunctionExpression rootFunction;
166+
167+
_InlineValueVisitor(this.collector, this.rootFunction);
168+
169+
@override
170+
void visitFormalParameter(FormalParameter node) {
171+
var name = node.name;
172+
if (name != null) {
173+
collector.recordVariableLookup(
174+
node.declaredFragment?.element,
175+
name.offset,
176+
name.length,
177+
);
178+
}
179+
super.visitFormalParameter(node);
180+
}
181+
182+
@override
183+
void visitFunctionExpression(FunctionExpression node) {
184+
// Don't descend into nested functions.
185+
if (node != rootFunction) {
186+
return;
187+
}
188+
189+
super.visitFunctionExpression(node);
190+
}
191+
192+
@override
193+
void visitSimpleIdentifier(SimpleIdentifier node) {
194+
switch (node.element) {
195+
case LocalVariableElement2(name3: _?):
196+
case FormalParameterElement():
197+
collector.recordVariableLookup(node.element, node.offset, node.length);
198+
}
199+
super.visitSimpleIdentifier(node);
200+
}
201+
202+
@override
203+
void visitVariableDeclaration(VariableDeclaration node) {
204+
var name = node.name;
205+
collector.recordVariableLookup(
206+
node.declaredElement2,
207+
name.offset,
208+
name.length,
209+
);
210+
super.visitVariableDeclaration(node);
211+
}
212+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import 'package:analysis_server/src/lsp/handlers/handler_implementation.dart';
4242
import 'package:analysis_server/src/lsp/handlers/handler_initialize.dart';
4343
import 'package:analysis_server/src/lsp/handlers/handler_initialized.dart';
4444
import 'package:analysis_server/src/lsp/handlers/handler_inlay_hint.dart';
45+
import 'package:analysis_server/src/lsp/handlers/handler_inline_value.dart';
4546
import 'package:analysis_server/src/lsp/handlers/handler_references.dart';
4647
import 'package:analysis_server/src/lsp/handlers/handler_rename.dart';
4748
import 'package:analysis_server/src/lsp/handlers/handler_selection_range.dart';
@@ -139,6 +140,7 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
139140
ImportsHandler.new,
140141
ImplementationHandler.new,
141142
IncomingCallHierarchyHandler.new,
143+
InlineValueHandler.new,
142144
OutgoingCallHierarchyHandler.new,
143145
PrepareCallHierarchyHandler.new,
144146
PrepareTypeHierarchyHandler.new,

pkg/analysis_server/lib/src/lsp/registration/feature_registration.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'package:analysis_server/src/lsp/handlers/handler_formatting.dart';
2424
import 'package:analysis_server/src/lsp/handlers/handler_hover.dart';
2525
import 'package:analysis_server/src/lsp/handlers/handler_implementation.dart';
2626
import 'package:analysis_server/src/lsp/handlers/handler_inlay_hint.dart';
27+
import 'package:analysis_server/src/lsp/handlers/handler_inline_value.dart';
2728
import 'package:analysis_server/src/lsp/handlers/handler_references.dart';
2829
import 'package:analysis_server/src/lsp/handlers/handler_rename.dart';
2930
import 'package:analysis_server/src/lsp/handlers/handler_selection_range.dart';
@@ -100,6 +101,7 @@ class LspFeatures {
100101
final HoverRegistrations hover;
101102
final ImplementationRegistrations implementation;
102103
final InlayHintRegistrations inlayHint;
104+
final InlineValueRegistrations inlineValue;
103105
final ReferencesRegistrations references;
104106
final RenameRegistrations rename;
105107
final SelectionRangeRegistrations selectionRange;
@@ -134,6 +136,7 @@ class LspFeatures {
134136
hover = HoverRegistrations(context),
135137
implementation = ImplementationRegistrations(context),
136138
inlayHint = InlayHintRegistrations(context),
139+
inlineValue = InlineValueRegistrations(context),
137140
references = ReferencesRegistrations(context),
138141
rename = RenameRegistrations(context),
139142
selectionRange = SelectionRangeRegistrations(context),
@@ -168,6 +171,7 @@ class LspFeatures {
168171
hover,
169172
implementation,
170173
inlayHint,
174+
inlineValue,
171175
references,
172176
rename,
173177
selectionRange,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ClientDynamicRegistrations {
2828
Method.textDocument_completion,
2929
Method.textDocument_hover,
3030
Method.textDocument_inlayHint,
31+
Method.textDocument_inlineValue,
3132
Method.textDocument_signatureHelp,
3233
Method.textDocument_references,
3334
Method.textDocument_documentHighlight,
@@ -104,6 +105,9 @@ class ClientDynamicRegistrations {
104105
bool get inlayHints =>
105106
_capabilities.textDocument?.inlayHint?.dynamicRegistration ?? false;
106107

108+
bool get inlineValue =>
109+
_capabilities.textDocument?.inlineValue?.dynamicRegistration ?? false;
110+
107111
bool get rangeFormatting =>
108112
_capabilities.textDocument?.rangeFormatting?.dynamicRegistration ?? false;
109113

@@ -190,6 +194,7 @@ class ServerCapabilitiesComputer {
190194
features.formatOnType.staticRegistration,
191195
documentRangeFormattingProvider: features.formatRange.staticRegistration,
192196
inlayHintProvider: features.inlayHint.staticRegistration,
197+
inlineValueProvider: features.inlineValue.staticRegistration,
193198
renameProvider: features.rename.staticRegistration,
194199
foldingRangeProvider: features.foldingRange.staticRegistration,
195200
selectionRangeProvider: features.selectionRange.staticRegistration,

pkg/analysis_server/test/lsp/initialization_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class InitializationTest extends AbstractLspAnalysisServerTest {
8686
if (options == null) {
8787
throw 'Registration options for $method were not found. '
8888
'Perhaps dynamicRegistration is missing from '
89-
'withAllSupportedTextDocumentDynamicRegistrations?';
89+
'setAllSupportedTextDocumentDynamicRegistrations?';
9090
}
9191
return TextDocumentRegistrationOptions.fromJson(
9292
options as Map<String, Object?>,

0 commit comments

Comments
 (0)