blob: 4900b762b398e4e0a1dc668ac3ce1723e4fe629a [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol.dart' hide MessageType;
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/client_configuration.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/extensions/positions.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
import 'package:analysis_server/src/services/correction/dart/convert_null_check_to_null_aware_element_or_entry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
typedef StaticOptions =
Either3<bool, InlineValueOptions, InlineValueRegistrationOptions>;
class InlineValueHandler
extends
SharedMessageHandler<InlineValueParams, TextDocumentInlineValueResult> {
InlineValueHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_inlineValue;
@override
LspJsonHandler<InlineValueParams> get jsonHandler =>
InlineValueParams.jsonHandler;
@override
bool get requiresTrustedCaller => false;
@override
Future<ErrorOr<TextDocumentInlineValueResult>> handle(
InlineValueParams params,
MessageInfo message,
CancellationToken token,
) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
var filePath = pathOfDoc(params.textDocument);
return filePath.mapResult((filePath) async {
var unitResult = await server.getResolvedUnit(filePath);
if (unitResult == null) {
return success(null);
}
var lineInfo = unitResult.lineInfo;
// We will provide values from the start of the visible range up to
// the end of the line the debugger is stopped on (which will do by just
// jumping to position 0 of the next line).
var visibleRange = params.range;
var stoppedLocation = params.context.stoppedLocation;
var applicableRange = Range(
start: visibleRange.start,
end: Position(line: stoppedLocation.end.line + 1, character: 0),
);
var stoppedOffset = toOffset(lineInfo, stoppedLocation.end);
return stoppedOffset.mapResult((stoppedOffset) async {
// Find the function that is executing. We will only show values for
// this single function expression.
var node = await server.getNodeAtOffset(filePath, stoppedOffset);
var function = node?.thisOrAncestorMatching(
(node) => node is FunctionExpression || node is MethodDeclaration,
);
if (function == null) {
return success(null);
}
var collector = _InlineValueCollector(lineInfo, applicableRange);
var visitor = _InlineValueVisitor(
server.lspClientConfiguration,
collector,
function,
);
function.accept(visitor);
return success(collector.values.values.toList());
});
});
}
}
class InlineValueRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
InlineValueRegistrations(super.info);
@override
ToJsonable? get options =>
TextDocumentRegistrationOptions(documentSelector: dartFiles);
@override
Method get registrationMethod => Method.textDocument_inlineValue;
@override
StaticOptions get staticOptions => Either3.t1(true);
@override
bool get supportsDynamic => clientDynamic.inlineValue;
}
/// Collects inline values, keeping only the most relevant where an element
/// is recorded multiple times.
class _InlineValueCollector {
/// A map of elements and their inline value.
final Map<Element2, InlineValue> values = {};
/// The range for which inline values should be retained.
///
/// This should be approximately the range of the visible code on screen up to
/// the point of execution.
final Range applicableRange;
/// A [LineInfo] used to convert offsets to lines/columns for comparing to
/// [applicableRange].
final LineInfo lineInfo;
_InlineValueCollector(this.lineInfo, this.applicableRange);
/// Records an expression inline value for [element] with [offset]/[length].
///
/// Expression values are sent to the client without expressions because the
/// client can use the range from the source to get the expression.
void recordExpression(Element2? element, int offset, int length) {
assert(offset >= 0);
assert(length > 0);
if (element == null) return;
var range = toRange(lineInfo, offset, length);
var value = InlineValue.t1(
InlineValueEvaluatableExpression(
range: range,
// We don't provide expression, because it always matches the source
// code and can be inferred.
),
);
_record(value, element);
}
/// Records a variable inline value for [element] with [offset]/[length].
///
/// Variable inline values are sent to the client without names because the
/// client can infer the name from the range and look it up from the debuggers
/// Scopes/Variables.
void recordVariableLookup(Element2? element, int offset, int length) {
assert(offset >= 0);
assert(length > 0);
if (element == null || element.isWildcardVariable) return;
var range = toRange(lineInfo, offset, length);
var value = InlineValue.t3(
InlineValueVariableLookup(
caseSensitiveLookup: true,
range: range,
// We don't provide name, because it always matches the source code
// for a variable and can be inferred.
),
);
_record(value, element);
}
/// Extracts the range from an [InlineValue].
Range _getRange(InlineValue value) {
return value.map(
(expression) => expression.range,
(text) => text.range,
(variable) => variable.range,
);
}
/// Records an inline value [value] for [element] if it is within range and is
/// the latest one in the source for that element.
void _record(InlineValue value, Element2 element) {
var range = _getRange(value);
// Never record anything outside of the visible range.
if (!range.intersects(applicableRange)) {
return;
}
// We only want to show each variable once, so keep only the one furthest
// into the source (closest to the execution pointer).
if (values[element] case var existingValue?) {
var existingPosition = _getRange(existingValue).start;
if (existingPosition.isAfterOrEqual(range.start)) {
return;
}
}
values[element] = value;
}
}
/// Visits a function expression and reports nodes that should have inline
/// values to [collector].
class _InlineValueVisitor extends GeneralizingAstVisitor<void> {
final LspClientConfiguration clientConfiguration;
final _InlineValueCollector collector;
final AstNode rootNode;
_InlineValueVisitor(this.clientConfiguration, this.collector, this.rootNode);
bool get experimentalInlineValuesProperties =>
clientConfiguration.global.experimentalInlineValuesProperties;
@override
void visitFormalParameter(FormalParameter node) {
var name = node.name;
if (name != null) {
collector.recordVariableLookup(
node.declaredFragment?.element,
name.offset,
name.length,
);
}
super.visitFormalParameter(node);
}
@override
void visitFunctionExpression(FunctionExpression node) {
// Don't descend into nested functions.
if (node != rootNode) {
return;
}
super.visitFunctionExpression(node);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
if (experimentalInlineValuesProperties) {
var parent = node.parent;
// Never produce values for the left side of a property access.
var isTarget = parent is PropertyAccess && node == parent.realTarget;
if (!isTarget) {
collector.recordExpression(node.element, node.offset, node.length);
}
}
super.visitPrefixedIdentifier(node);
}
@override
void visitPropertyAccess(PropertyAccess node) {
if (experimentalInlineValuesProperties && node.target is Identifier) {
collector.recordExpression(
node.canonicalElement,
node.offset,
node.length,
);
}
super.visitPropertyAccess(node);
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
var parent = node.parent;
// Never produce values for the left side of a prefixed identifier.
// Or parts of an invocation.
var isTarget = parent is PrefixedIdentifier && node == parent.prefix;
var isInvocation = parent is InvocationExpression;
if (!isTarget && !isInvocation) {
switch (node.element) {
case LocalVariableElement2(name3: _?):
case FormalParameterElement():
collector.recordVariableLookup(
node.element,
node.offset,
node.length,
);
}
}
super.visitSimpleIdentifier(node);
}
@override
void visitVariableDeclaration(VariableDeclaration node) {
var name = node.name;
collector.recordVariableLookup(
node.declaredElement2,
name.offset,
name.length,
);
super.visitVariableDeclaration(node);
}
}