Skip to content

Commit

Permalink
[anlaysis_server] Add support for LSP's Go-to-Type Definition
Browse files Browse the repository at this point in the history
Fixes Dart-Code/Dart-Code#3833.

Change-Id: Ieb459ded5af3e074566b69c7df40f1bf6b5d6c25
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/239306
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
  • Loading branch information
DanTup authored and Commit Bot committed Mar 29, 2022
1 parent a1bf2b1 commit e7adcb6
Show file tree
Hide file tree
Showing 17 changed files with 535 additions and 17 deletions.
4 changes: 2 additions & 2 deletions pkg/analysis_server/lib/lsp_protocol/protocol_special.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ class Either2<T1, T2> {
final T1? _t1;
final T2? _t2;

Either2.t1(T1 this._t1)
const Either2.t1(T1 this._t1)
: _t2 = null,
_which = 1;
Either2.t2(T2 this._t2)
const Either2.t2(T2 this._t2)
: _t1 = null,
_which = 2;

Expand Down
3 changes: 3 additions & 0 deletions pkg/analysis_server/lib/src/lsp/client_capabilities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class LspClientCapabilities {
final bool literalCodeActions;
final bool insertReplaceCompletionRanges;
final bool definitionLocationLink;
final bool typeDefinitionLocationLink;
final bool hierarchicalSymbols;
final bool diagnosticCodeDescription;
final Set<CodeActionKind> codeActionKinds;
Expand Down Expand Up @@ -109,6 +110,8 @@ class LspClientCapabilities {
false,
definitionLocationLink =
raw.textDocument?.definition?.linkSupport ?? false,
typeDefinitionLocationLink =
raw.textDocument?.typeDefinition?.linkSupport ?? false,
completionItemTags = _listToSet(
raw.textDocument?.completion?.completionItem?.tagSupport?.valueSet),
diagnosticTags = _listToSet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ abstract class SimpleEditCommandHandler
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

final lineInfo = unit.lineInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ class CodeActionHandler extends MessageHandler<CodeActionParams,
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

final supportsApplyEdit = clientCapabilities.applyEdit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ class CompletionHandler extends MessageHandler<CompletionParams, CompletionList>
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

final triggerCharacter = params.context?.triggerCharacter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,7 @@ class CompletionResolveHandler
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

final file = data.file;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ class DefinitionHandler extends MessageHandler<TextDocumentPositionParams,
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

final supportsLocationLink = clientCapabilities.definitionLocationLink;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ class SignatureHelpHandler
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

// If triggered automatically by pressing the trigger character, we will
Expand Down
2 changes: 2 additions & 0 deletions pkg/analysis_server/lib/src/lsp/handlers/handler_states.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import 'package:analysis_server/src/lsp/handlers/handler_semantic_tokens.dart';
import 'package:analysis_server/src/lsp/handlers/handler_shutdown.dart';
import 'package:analysis_server/src/lsp/handlers/handler_signature_help.dart';
import 'package:analysis_server/src/lsp/handlers/handler_text_document_changes.dart';
import 'package:analysis_server/src/lsp/handlers/handler_type_definition.dart';
import 'package:analysis_server/src/lsp/handlers/handler_will_rename_files.dart';
import 'package:analysis_server/src/lsp/handlers/handler_workspace_configuration.dart';
import 'package:analysis_server/src/lsp/handlers/handler_workspace_symbols.dart';
Expand Down Expand Up @@ -82,6 +83,7 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
registerHandler(DocumentColorPresentationHandler(server));
registerHandler(SignatureHelpHandler(server));
registerHandler(DefinitionHandler(server));
registerHandler(TypeDefinitionHandler(server));
registerHandler(SuperHandler(server));
registerHandler(ReferencesHandler(server));
registerHandler(ImplementationHandler(server));
Expand Down
166 changes: 166 additions & 0 deletions pkg/analysis_server/lib/src/lsp/handlers/handler_type_definition.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) 2022, 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';
import 'package:analysis_server/lsp_protocol/protocol_special.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/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/element.dart' show ElementImpl;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/utilities/analyzer_converter.dart';

typedef _LocationsOrLinks = Either2<List<Location>, List<LocationLink>>;

class TypeDefinitionHandler
extends MessageHandler<TypeDefinitionParams, _LocationsOrLinks>
with LspPluginRequestHandlerMixin {
static const _emptyResult = _LocationsOrLinks.t1([]);

TypeDefinitionHandler(LspAnalysisServer server) : super(server);

@override
Method get handlesMessage => Method.textDocument_typeDefinition;

@override
LspJsonHandler<TypeDefinitionParams> get jsonHandler =>
TypeDefinitionParams.jsonHandler;

@override
Future<ErrorOr<_LocationsOrLinks>> handle(
TypeDefinitionParams params, CancellationToken token) async {
if (!isDartDocument(params.textDocument)) {
return success(_emptyResult);
}

final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return serverNotInitializedError;
}

/// Whether the client supports `LocationLink` results instead of the
/// original `Location`. `LocationLink`s can include an additional `Range`
/// to distinguish between codeRange and nameRange (selectionRange), and
/// also an `originSelectionRange` that tells the client which range the
/// result is valid for.
final supportsLocationLink = clientCapabilities.typeDefinitionLocationLink;
final pos = params.position;
final path = pathOfDoc(params.textDocument);

return path.mapResult((path) async {
final result = await server.getResolvedUnit(path);
if (result == null) {
return success(_emptyResult);
}

final offset = toOffset(result.lineInfo, pos);
return offset.mapResult((offset) async {
final node = NodeLocator(offset).searchWithin(result.unit);
if (node == null) {
return success(_emptyResult);
}

final type = node is Expression ? _getType(node) : null;
final element = type?.element;
if (element is! ElementImpl) {
return success(_emptyResult);
}

// Obtain a `LineInfo` for the targets file to map offsets.
final targetUnitElement =
element.thisOrAncestorOfType<CompilationUnitElement>();
final targetLineInfo = targetUnitElement?.lineInfo;
if (targetLineInfo == null) {
return success(_emptyResult);
}

final converter = AnalyzerConverter();
final location = converter.locationFromElement(element);
if (location == null) {
return success(_emptyResult);
}

if (supportsLocationLink) {
return success(_LocationsOrLinks.t2([
_toLocationLink(
result.lineInfo, targetLineInfo, node, element, location)
]));
} else {
return success(
_LocationsOrLinks.t1([_toLocation(location, targetLineInfo)]));
}
});
});
}

/// Creates an LSP [Location] for the server [location].
Location _toLocation(plugin.Location location, LineInfo lineInfo) {
return Location(
uri: Uri.file(location.file).toString(),
range: toRange(lineInfo, location.offset, location.length),
);
}

/// Creates an LSP [LocationLink] for the server [targetLocation].
///
/// Uses [originLineInfo] and [originNode] to compute `originSelectionRange`
/// and [targetLineInfo] and [targetElement] for code ranges.
LocationLink _toLocationLink(
LineInfo originLineInfo,
LineInfo targetLineInfo,
AstNode originNode,
ElementImpl targetElement,
plugin.Location targetLocation,
) {
final nameRange =
toRange(targetLineInfo, targetLocation.offset, targetLocation.length);

final codeOffset = targetElement.codeOffset;
final codeLength = targetElement.codeLength;
final codeRange = codeOffset != null && codeLength != null
? toRange(targetLineInfo, codeOffset, codeLength)
: nameRange;

return LocationLink(
originSelectionRange:
toRange(originLineInfo, originNode.offset, originNode.length),
targetUri: Uri.file(targetLocation.file).toString(),
targetRange: codeRange,
targetSelectionRange: nameRange,
);
}

/// Returns the [DartType] most appropriate for navigating to from [node] when
/// invoking Go to Type Definition.
static DartType? _getType(Expression node) {
if (node is SimpleIdentifier) {
final element = node.staticElement;
if (element is ClassElement) {
return element.thisType;
} else if (element is VariableElement) {
if (node.inDeclarationContext()) {
return element.type;
}
final parent = node.parent?.parent;
if (parent is NamedExpression && parent.name.label == node) {
return element.type;
}
} else if (node.inSetterContext()) {
final writeElement = node.writeElement;
if (writeElement is PropertyAccessorElement) {
return writeElement.variable.type;
}
}
}

return node.staticType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ class WorkspaceSymbolHandler
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return error(ErrorCodes.ServerNotInitialized,
'Requests not before server is initilized');
return serverNotInitializedError;
}

// Respond to empty queries with an empty list. The spec says this should
Expand Down
3 changes: 3 additions & 0 deletions pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ mixin Handler<P, R> {
final fileModifiedError = error<R>(ErrorCodes.ContentModified,
'Document was modified before operation completed', null);

final serverNotInitializedError = error<R>(ErrorCodes.ServerNotInitialized,
'Request not valid before server is initialized');

LspAnalysisServer get server;

bool fileHasBeenModified(String path, num? clientVersion) {
Expand Down
11 changes: 11 additions & 0 deletions pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ClientDynamicRegistrations {
Method.textDocument_rename,
Method.textDocument_foldingRange,
Method.textDocument_selectionRange,
Method.textDocument_typeDefinition,
// workspace.fileOperations covers all file operation methods but we only
// support this one.
Method.workspace_willRenameFiles,
Expand Down Expand Up @@ -109,6 +110,9 @@ class ClientDynamicRegistrations {
bool get textSync =>
_capabilities.textDocument?.synchronization?.dynamicRegistration ?? false;

bool get typeDefinition =>
_capabilities.textDocument?.typeDefinition?.dynamicRegistration ?? false;

bool get typeFormatting =>
_capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
false;
Expand Down Expand Up @@ -449,6 +453,13 @@ class ServerCapabilitiesComputer {
Method.textDocument_definition,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.typeDefinition,
Method.textDocument_typeDefinition,
TextDocumentRegistrationOptions(
documentSelector: [dartFiles], // This one is currently Dart-specific
),
);
register(
dynamicRegistrations.implementation,
Method.textDocument_implementation,
Expand Down
39 changes: 39 additions & 0 deletions pkg/analysis_server/test/lsp/server_abstract.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ mixin ClientCapabilitiesHelperMixin {
formats: [],
tokenModifiers: [],
tokenTypes: []).toJson(),
'typeDefinition': {'dynamicRegistration': true},
});
}

Expand Down Expand Up @@ -510,6 +511,7 @@ mixin ClientCapabilitiesHelperMixin {
) {
return extendTextDocumentCapabilities(source, {
'definition': {'linkSupport': true},
'typeDefinition': {'linkSupport': true},
'implementation': {'linkSupport': true}
});
}
Expand Down Expand Up @@ -1294,6 +1296,43 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
return expectSuccessfulResponseTo(request, Location.fromJson);
}

Future<Either2<List<Location>, List<LocationLink>>> getTypeDefinition(
Uri uri, Position pos) {
final request = makeRequest(
Method.textDocument_typeDefinition,
TypeDefinitionParams(
textDocument: TextDocumentIdentifier(uri: uri.toString()),
position: pos,
),
);
return expectSuccessfulResponseTo(
request,
_generateFromJsonFor(
_canParseList(Location.canParse),
_fromJsonList(Location.fromJson),
_canParseList(LocationLink.canParse),
_fromJsonList(LocationLink.fromJson)),
);
}

Future<List<Location>> getTypeDefinitionAsLocation(
Uri uri, Position pos) async {
final results = await getTypeDefinition(uri, pos);
return results.map(
(locations) => locations,
(locationLinks) => throw 'Expected List<Location> got List<LocationLink>',
);
}

Future<List<LocationLink>> getTypeDefinitionAsLocationLinks(
Uri uri, Position pos) async {
final results = await getTypeDefinition(uri, pos);
return results.map(
(locations) => throw 'Expected List<LocationLink> got List<Location>',
(locationLinks) => locationLinks,
);
}

Future<List<SymbolInformation>> getWorkspaceSymbols(String query) {
final request = makeRequest(
Method.workspace_symbol,
Expand Down

0 comments on commit e7adcb6

Please sign in to comment.