Skip to content

Commit 5681cfa

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Provide better values for active parameters in signature help
We were previously using a -1 because we don't support this, but it turns out the LSP spec changed from "number" to "uinteger" in v3.16, meaning -1 would violate some assumptions clients made. For the case where we don't have a value, we now just use the `length` of the `parameters` list (eg. the first invalid value, which is valid according to the spec and can indicate to the client that there is no active parameter*), however I thought we could also do better by providing values for some cases (named params that we can match up, and positional params by counting only the positional params+args and matching them up). \* The spec says clients can treat out-of-range values as 0, however VS Code doesn't highlight any parameters in that case, which is better IMO. Fixes #58577 Change-Id: I1de7b0eaacf06aac7e302ae9095342f4e4d7a466 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/396840 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent f331924 commit 5681cfa

File tree

7 files changed

+520
-178
lines changed

7 files changed

+520
-178
lines changed

pkg/analysis_server/lib/src/cider/signature_help.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class CiderSignatureHelpComputer {
5353
if (signature != null) {
5454
return SignatureHelpResponse(
5555
toSignatureHelp(formats, signature),
56-
lineInfo.getLocation(computer.argumentList.offset + 1),
56+
lineInfo.getLocation(signature.argumentList.offset + 1),
5757
);
5858
}
5959
}

pkg/analysis_server/lib/src/computer/computer_signature.dart

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,34 @@ import 'package:analyzer/dart/ast/ast.dart';
99
import 'package:analyzer/dart/element/element2.dart';
1010
import 'package:analyzer/dart/element/type.dart';
1111
import 'package:analyzer/src/dart/ast/element_locator.dart';
12-
import 'package:analyzer/src/dart/ast/utilities.dart';
1312
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
13+
import 'package:analyzer/src/utilities/extensions/ast.dart';
1414

1515
/// A computer for the signature at the specified offset of a Dart
1616
/// [CompilationUnit].
1717
class DartUnitSignatureComputer {
1818
final AstNode? _node;
19-
late ArgumentList _argumentList;
19+
final int _offset;
2020
final DocumentationPreference documentationPreference;
2121
final DartDocumentationComputer _documentationComputer;
2222

2323
DartUnitSignatureComputer(
2424
DartdocDirectiveInfo dartdocInfo,
2525
CompilationUnit unit,
26-
int offset, {
26+
this._offset, {
2727
this.documentationPreference = DocumentationPreference.full,
2828
}) : _documentationComputer = DartDocumentationComputer(dartdocInfo),
29-
_node = NodeLocator(offset).searchWithin(unit);
30-
31-
/// The [ArgumentList] node located by [compute].
32-
ArgumentList get argumentList => _argumentList;
29+
_node = unit.nodeCovering(offset: _offset);
3330

3431
bool get offsetIsValid => _node != null;
3532

3633
/// Returns the computed signature information, maybe `null`.
37-
AnalysisGetSignatureResult? compute() {
38-
var argumentList = _findArgumentList();
39-
if (argumentList == null) {
34+
SignatureInformation? compute() {
35+
var argumentAndList = _findArgumentAndList();
36+
if (argumentAndList == null) {
4037
return null;
4138
}
39+
var (argumentList, argument) = argumentAndList;
4240
String? name;
4341
Element2? element;
4442
List<FormalParameterElement>? parameters;
@@ -77,16 +75,105 @@ class DartUnitSignatureComputer {
7775
return null;
7876
}
7977

80-
_argumentList = argumentList;
81-
var convertedParameters = parameters.map((p) => _convertParam(p)).toList();
78+
// Try to compute the active parameter so the IDE can highlight it.
79+
int? activeParameterIndex;
80+
if (argument case Expression(:var correspondingParameter?)) {
81+
// If we know the active parameter, use its index.
82+
activeParameterIndex = parameters.indexOf(correspondingParameter);
83+
} else if (argument is! NamedExpression) {
84+
// If we're not a named expression, then we can count how many positional
85+
// parameters there are before us, and then find the index of the same
86+
// index positional parameter.
87+
var positionalArgsToSkip =
88+
argumentList.arguments
89+
.where((argument) => argument is! NamedExpression)
90+
.takeWhile((argument) => argument.end < _offset)
91+
.length;
92+
for (var i = 0; i < parameters.length; i++) {
93+
if (parameters[i].isPositional) {
94+
// This is the first positional parameter after our skips, so this is
95+
// the active parameter.
96+
if (positionalArgsToSkip == 0) {
97+
activeParameterIndex = i;
98+
break;
99+
}
100+
positionalArgsToSkip--;
101+
}
102+
}
103+
}
104+
82105
var dartdoc = _documentationComputer.computePreferred2(
83106
element,
84107
documentationPreference,
85108
);
86109

110+
return SignatureInformation(
111+
name: name,
112+
parameters: parameters,
113+
argumentList: argumentList,
114+
activeParameterIndex: activeParameterIndex,
115+
dartdoc: dartdoc,
116+
);
117+
}
118+
119+
/// Return the closest argument list surrounding the [_node] and the node for
120+
/// the active argument (if there is one).
121+
(ArgumentList, AstNode?)? _findArgumentAndList() {
122+
var node = _node;
123+
while (node != null) {
124+
// Certain nodes don't make sense to search above for an argument list
125+
// (for example when inside a function expression).
126+
if (node is FunctionExpression) {
127+
return null;
128+
}
129+
130+
if (node is ArgumentList) {
131+
return (node, null);
132+
}
133+
if (node.parent case ArgumentList list) {
134+
return (list, node);
135+
}
136+
137+
node = node.parent;
138+
}
139+
return null;
140+
}
141+
}
142+
143+
/// Information about a function signature.
144+
class SignatureInformation {
145+
/// The name of the function/method.
146+
final String name;
147+
148+
/// The parameters for the function/method.
149+
final List<FormalParameterElement> parameters;
150+
151+
/// The current argument list at the invocation site.
152+
final ArgumentList argumentList;
153+
154+
/// Documentation for the function/method.
155+
final String? dartdoc;
156+
157+
/// The index in [parameters] for the parameter that matches where the offset
158+
/// was in the invocation list.
159+
///
160+
/// This is only supplied when it can be computed. Positional arguments past
161+
/// the number of positional parameters or named arguments with no matching
162+
/// name will not be returned.
163+
final int? activeParameterIndex;
164+
165+
SignatureInformation({
166+
required this.name,
167+
required this.parameters,
168+
required this.argumentList,
169+
required this.activeParameterIndex,
170+
required this.dartdoc,
171+
});
172+
173+
AnalysisGetSignatureResult toLegacyProtocol() {
87174
return AnalysisGetSignatureResult(
88175
name,
89-
convertedParameters,
176+
parameters.map(_convertParam).toList(),
90177
dartdoc: dartdoc,
91178
);
92179
}
@@ -105,18 +192,4 @@ class DartUnitSignatureComputer {
105192
defaultValue: param.defaultValueCode,
106193
);
107194
}
108-
109-
/// Return the closest argument list surrounding the [_node].
110-
ArgumentList? _findArgumentList() {
111-
var node = _node;
112-
while (node != null && node is! ArgumentList) {
113-
// Certain nodes don't make sense to search above for an argument list
114-
// (for example when inside a function expression).
115-
if (node is FunctionExpression) {
116-
return null;
117-
}
118-
node = node.parent;
119-
}
120-
return node as ArgumentList?;
121-
}
122195
}

pkg/analysis_server/lib/src/computer/computer_type_arguments_signature.dart

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,23 @@ class DartTypeArgumentsSignatureComputer {
9797
)
9898
.toList();
9999

100-
var signatures = [
101-
lsp.SignatureInformation(
102-
label: label,
103-
documentation:
104-
documentation != null
105-
? asMarkupContentOrString(preferredFormats, documentation)
106-
: null,
107-
parameters: parameters,
108-
),
109-
];
100+
var signature = lsp.SignatureInformation(
101+
label: label,
102+
documentation:
103+
documentation != null
104+
? asMarkupContentOrString(preferredFormats, documentation)
105+
: null,
106+
parameters: parameters,
107+
);
110108

111109
return lsp.SignatureHelp(
112-
signatures: signatures,
110+
signatures: [signature],
113111
activeSignature: 0,
114-
// As with [toSignatureHelp], we don't currently use this, but need to set
115-
// it to something that doesn't match a parameter to avoid one being
116-
// selected.
117-
activeParameter: -1,
112+
// This must be a unsigned integer but can be out of range. Since we don't
113+
// currently support this, just provide the first out-of-bounds value and
114+
// allow the client to decide what to do (the LSP spec says it can be
115+
// treated as 0, but VS Code will not highlight any, which is preferred).
116+
activeParameter: signature.parameters?.length ?? 0,
118117
);
119118
}
120119
}

pkg/analysis_server/lib/src/handler/legacy/analysis_get_signature.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ class AnalysisGetSignatureHandler extends LegacyHandler {
5959
return;
6060
}
6161

62-
sendResult(signature);
62+
sendResult(signature.toLegacyProtocol());
6363
}
6464
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class SignatureHelpHandler
9797
// argument list.
9898
// The ArgumentList's offset is before the paren, but the request offset
9999
// will be after.
100-
if (autoTriggered && offset != computer.argumentList.offset + 1) {
100+
if (autoTriggered && offset != signature.argumentList.offset + 1) {
101101
return success(null);
102102
}
103103

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

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:analysis_server/lsp_protocol/protocol.dart' hide Declaration;
77
import 'package:analysis_server/src/analysis_server.dart';
88
import 'package:analysis_server/src/collections.dart';
99
import 'package:analysis_server/src/computer/computer_documentation.dart';
10+
import 'package:analysis_server/src/computer/computer_signature.dart' as server;
1011
import 'package:analysis_server/src/lsp/client_capabilities.dart';
1112
import 'package:analysis_server/src/lsp/constants.dart' as lsp;
1213
import 'package:analysis_server/src/lsp/constants.dart';
@@ -525,11 +526,7 @@ lsp.Location? fragmentToLocation(
525526

526527
return lsp.Location(
527528
uri: uriConverter.toClientUri(sourcePath),
528-
range: toRange(
529-
libraryFragment.lineInfo,
530-
nameOffset,
531-
nameLength,
532-
),
529+
range: toRange(libraryFragment.lineInfo, nameOffset, nameLength),
533530
);
534531
}
535532

@@ -1519,40 +1516,29 @@ lsp.Range toRange(server.LineInfo lineInfo, int offset, int length) {
15191516

15201517
lsp.SignatureHelp toSignatureHelp(
15211518
Set<lsp.MarkupKind>? preferredFormats,
1522-
server.AnalysisGetSignatureResult signature,
1519+
server.SignatureInformation signature,
15231520
) {
15241521
// For now, we only support returning one (though we may wish to use named
15251522
// args. etc. to provide one for each possible "next" option when the cursor
15261523
// is at the end ready to provide another argument).
15271524

15281525
/// Gets the label for an individual parameter in the form
15291526
/// String s = 'foo'
1530-
String getParamLabel(server.ParameterInfo p) {
1531-
var def = p.defaultValue != null ? ' = ${p.defaultValue}' : '';
1532-
var prefix =
1533-
p.kind == server.ParameterKind.REQUIRED_NAMED ? 'required ' : '';
1534-
return '$prefix${p.type} ${p.name}$def';
1527+
String getParamLabel(FormalParameterElement p) {
1528+
var defaultCodeSuffix =
1529+
p.defaultValueCode != null ? ' = ${p.defaultValueCode}' : '';
1530+
var requiredPrefix = p.isRequiredNamed ? 'required ' : '';
1531+
return '$requiredPrefix${p.type} ${p.displayName}$defaultCodeSuffix';
15351532
}
15361533

15371534
/// Gets the full signature label in the form
15381535
/// foo(String s, int i, bool a = true)
1539-
String getSignatureLabel(server.AnalysisGetSignatureResult resp) {
1536+
String getSignatureLabel(server.SignatureInformation resp) {
15401537
var positionalRequired =
1541-
signature.parameters
1542-
.where((p) => p.kind == server.ParameterKind.REQUIRED_POSITIONAL)
1543-
.toList();
1538+
signature.parameters.where((p) => p.isRequiredPositional).toList();
15441539
var positionalOptional =
1545-
signature.parameters
1546-
.where((p) => p.kind == server.ParameterKind.OPTIONAL_POSITIONAL)
1547-
.toList();
1548-
var named =
1549-
signature.parameters
1550-
.where(
1551-
(p) =>
1552-
p.kind == server.ParameterKind.OPTIONAL_NAMED ||
1553-
p.kind == server.ParameterKind.REQUIRED_NAMED,
1554-
)
1555-
.toList();
1540+
signature.parameters.where((p) => p.isOptionalPositional).toList();
1541+
var named = signature.parameters.where((p) => p.isNamed).toList();
15561542
var params = [];
15571543
if (positionalRequired.isNotEmpty) {
15581544
params.add(positionalRequired.map(getParamLabel).join(', '));
@@ -1566,7 +1552,7 @@ lsp.SignatureHelp toSignatureHelp(
15661552
return '${resp.name}(${params.join(", ")})';
15671553
}
15681554

1569-
lsp.ParameterInformation toParameterInfo(server.ParameterInfo param) {
1555+
lsp.ParameterInformation toParameterInfo(FormalParameterElement param) {
15701556
// LSP 3.14.0 supports providing label offsets (to avoid clients having
15711557
// to guess based on substrings). We should check the
15721558
// signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport
@@ -1588,15 +1574,15 @@ lsp.SignatureHelp toSignatureHelp(
15881574
),
15891575
],
15901576
activeSignature: 0, // activeSignature
1591-
// TODO(dantup): The LSP spec says this value will default to 0 if it's
1592-
// not supplied or outside of the value range. However, setting -1 results
1593-
// in no parameters being selected in VS Code, whereas null/0 will select the first.
1594-
// We'd like for none to be selected (since we don't support this yet) so
1595-
// we send -1. I've made a request for LSP to support not selecting a parameter
1596-
// (because you could also be on param 5 of an invalid call to a function
1597-
// taking only 3 arguments) here:
1598-
// https://github.com/Microsoft/language-server-protocol/issues/456#issuecomment-452318297
1599-
activeParameter: -1, // activeParameter
1577+
// We must provide a unsigned integer here but it's possible there isn't
1578+
// a valid value (because the user might be in the 10th argument of an
1579+
// invocation that only takes 1). The LSP spec allows us to send an
1580+
// out-of-bounds value so send the first out-of-bound value (`.length`). The
1581+
// spec says this may be treated as 0, however VS Code will not highlight
1582+
// any parameter in this case (which is preferred and hopefully other
1583+
// clients may copy).
1584+
activeParameter:
1585+
signature.activeParameterIndex ?? signature.parameters.length,
16001586
);
16011587
}
16021588

0 commit comments

Comments
 (0)