[analysis_server] Support LSP SignatureHelp for type argument lists

See https://github.com/Dart-Code/Dart-Code/issues/2834.

Change-Id: Idce0d0b133b1f40c1953e5f1c5f4531701a68c56
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/204404
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Danny Tuppeny 2021-06-22 14:56:44 +00:00 committed by commit-bot@chromium.org
parent e1620d6a27
commit ad6c732663
4 changed files with 250 additions and 6 deletions

View file

@ -28,7 +28,6 @@ class DartUnitSignatureComputer {
bool get offsetIsValid => _node != null;
// Return the closest argument list surrounding the [_node].
/// Returns the computed signature information, maybe `null`.
AnalysisGetSignatureResult? compute() {
var argumentList = _findArgumentList();
@ -80,6 +79,7 @@ class DartUnitSignatureComputer {
defaultValue: param.defaultValueCode);
}
/// Return the closest argument list surrounding the [_node].
ArgumentList? _findArgumentList() {
var node = _node;
while (node != null && node is! ArgumentList) {

View file

@ -0,0 +1,116 @@
// Copyright (c) 2021, 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_generated.dart' as lsp;
import 'package:analysis_server/src/computer/computer_hover.dart';
import 'package:analysis_server/src/lsp/dartdoc.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/element_locator.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
/// A computer for the signature help information about the type parameters for
/// the [TypeArgumentList] surrounding the specified offset of a Dart
/// [CompilationUnit].
class DartTypeArgumentsSignatureComputer {
final DartdocDirectiveInfo _dartdocInfo;
final AstNode? _node;
final Set<lsp.MarkupKind>? preferredFormats;
late TypeArgumentList _argumentList;
final bool _isNonNullableByDefault;
DartTypeArgumentsSignatureComputer(
this._dartdocInfo,
CompilationUnit _unit,
int _offset,
this.preferredFormats,
) : _node = NodeLocator(_offset).searchWithin(_unit),
_isNonNullableByDefault = _unit.isNonNullableByDefault;
/// The [TypeArgumentList] node located by [compute].
TypeArgumentList get argumentList => _argumentList;
bool get offsetIsValid => _node != null;
/// Returns the computed signature information, maybe `null`.
lsp.SignatureHelp? compute() {
var argumentList = _findTypeArgumentList();
if (argumentList == null) {
return null;
}
final parent = argumentList.parent;
Element? element;
if (parent is TypeName) {
element = ElementLocator.locate(parent.name);
} else if (parent is MethodInvocation) {
element = ElementLocator.locate(parent.methodName);
}
if (element is! TypeParameterizedElement ||
element.typeParameters.isEmpty) {
return null;
}
_argumentList = argumentList;
final label =
element.getDisplayString(withNullability: _isNonNullableByDefault);
final documentation =
DartUnitHoverComputer.computeDocumentation(_dartdocInfo, element)?.full;
return _toSignatureHelp(
label,
cleanDartdoc(documentation),
element.typeParameters,
);
}
/// Return the closest type argument list surrounding the [_node].
TypeArgumentList? _findTypeArgumentList() {
var node = _node;
while (node != null && node is! TypeArgumentList) {
// Certain nodes don't make sense to search above for an argument list
// (for example when inside a function expression).
if (node is FunctionExpression) {
return null;
}
node = node.parent;
}
return node as TypeArgumentList?;
}
/// Builds an [lsp.SignatureHelp] for the given type parameters.
lsp.SignatureHelp? _toSignatureHelp(
String label,
String? documentation,
List<TypeParameterElement> typeParameters,
) {
final parameters = typeParameters
.map((param) => lsp.ParameterInformation(
label: param.getDisplayString(
withNullability: _isNonNullableByDefault),
))
.toList();
final signatures = [
lsp.SignatureInformation(
label: label,
documentation: documentation != null
? asStringOrMarkupContent(preferredFormats, documentation)
: null,
parameters: parameters,
),
];
return lsp.SignatureHelp(
signatures: signatures,
activeSignature: 0,
// As with [toSignatureHelp], we don't currently use this, but need to set
// it to something that doesn't match a parameter to avoid one being
// selected.
activeParameter: -1,
);
}
}

View file

@ -5,9 +5,12 @@
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/computer/computer_signature.dart';
import 'package:analysis_server/src/computer/computer_type_arguments_signature.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
class SignatureHelpHandler
extends MessageHandler<SignatureHelpParams, SignatureHelp?> {
@ -52,10 +55,24 @@ class SignatureHelpHandler
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
return offset.mapResult((offset) {
final computer = DartUnitSignatureComputer(
server.getDartdocDirectiveInfoFor(unit.result),
unit.result.unit!,
offset);
final formats = clientCapabilities.signatureHelpDocumentationFormats;
final dartDocInfo = server.getDartdocDirectiveInfoFor(unit.result);
// First check if we're in a type args list and if so build some
// signature help for that.
final typeArgsSignature = _tryGetTypeArgsSignatureHelp(
dartDocInfo,
unit.result.unit!,
offset,
autoTriggered,
formats,
);
if (typeArgsSignature != null) {
return success(typeArgsSignature);
}
final computer =
DartUnitSignatureComputer(dartDocInfo, unit.result.unit!, offset);
if (!computer.offsetIsValid) {
return success(null); // No error, just no valid hover.
}
@ -72,8 +89,39 @@ class SignatureHelpHandler
return success(null);
}
final formats = clientCapabilities.signatureHelpDocumentationFormats;
return success(toSignatureHelp(formats, signature));
});
}
/// Tries to create signature information for a surrounding [TypeArgumentList].
///
/// Returns `null` if [offset] is in an invalid location, not inside a type
/// argument list or was auto-triggered in a location that was not the start
/// of a type argument list.
SignatureHelp? _tryGetTypeArgsSignatureHelp(
DartdocDirectiveInfo dartDocInfo,
CompilationUnit unit,
int offset,
bool autoTriggered,
Set<MarkupKind>? formats,
) {
final typeArgsComputer =
DartTypeArgumentsSignatureComputer(dartDocInfo, unit, offset, formats);
if (!typeArgsComputer.offsetIsValid) {
return null;
}
final typeSignature = typeArgsComputer.compute();
if (typeSignature == null) {
return null;
}
// If auto-triggered from typing a `<`, only show if that `<` was at
// the start of the arg list (to avoid triggering on other `<`s).
if (autoTriggered && offset != typeArgsComputer.argumentList.offset + 1) {
return null;
}
return typeSignature;
}
}

View file

@ -465,6 +465,86 @@ void f() {
));
}
Future<void> test_typeParams_class() async {
final content = '''
/// My Foo.
class Foo<T1, T2 extends String> {}
class Bar extends Foo<^> {}
''';
const expectedLabel = 'class Foo<T1, T2 extends String>';
const expectedDoc = 'My Foo.';
await initialize(
textDocumentCapabilities: withSignatureHelpContentFormat(
emptyTextDocumentClientCapabilities, [MarkupKind.Markdown]));
await openFile(mainFileUri, withoutMarkers(content));
await testSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_typeParams_function() async {
final content = '''
/// My Foo.
void foo<T1, T2 extends String>() {
foo<^>();
}
''';
const expectedLabel = 'void foo<T1, T2 extends String>()';
const expectedDoc = 'My Foo.';
await initialize(
textDocumentCapabilities: withSignatureHelpContentFormat(
emptyTextDocumentClientCapabilities, [MarkupKind.Markdown]));
await openFile(mainFileUri, withoutMarkers(content));
await testSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_typeParams_method() async {
final content = '''
class Foo {
/// My Foo.
void foo<T1, T2 extends String>() {
foo<^>();
}
}
''';
const expectedLabel = 'void foo<T1, T2 extends String>()';
const expectedDoc = 'My Foo.';
await initialize(
textDocumentCapabilities: withSignatureHelpContentFormat(
emptyTextDocumentClientCapabilities, [MarkupKind.Markdown]));
await openFile(mainFileUri, withoutMarkers(content));
await testSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_unopenFile() async {
final content = '''
/// Does foo.