mirror of
https://github.com/dart-lang/sdk
synced 2024-09-15 23:39:48 +00:00
[analysis_server] Add LSP Type Hierarchy
Fixes https://github.com/Dart-Code/Dart-Code/issues/3313. Fixes https://github.com/Dart-Code/Dart-Code/issues/2527. Change-Id: I9f471fd3d7d55999795fee7ab4761e906566bd10 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264002 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
parent
b5846d8aba
commit
6f8d1e4859
|
@ -167,6 +167,13 @@ class TypeHierarchyItem {
|
||||||
/// The range of the code for the declaration of this item.
|
/// The range of the code for the declaration of this item.
|
||||||
final SourceRange codeRange;
|
final SourceRange codeRange;
|
||||||
|
|
||||||
|
TypeHierarchyItem({
|
||||||
|
required this.displayName,
|
||||||
|
required this.file,
|
||||||
|
required this.nameRange,
|
||||||
|
required this.codeRange,
|
||||||
|
});
|
||||||
|
|
||||||
TypeHierarchyItem.forElement(Element element)
|
TypeHierarchyItem.forElement(Element element)
|
||||||
: displayName = element.displayName,
|
: displayName = element.displayName,
|
||||||
nameRange = _nameRangeForElement(element),
|
nameRange = _nameRangeForElement(element),
|
||||||
|
|
|
@ -243,37 +243,33 @@ abstract class _AbstractCallHierarchyCallsHandler<P, R, C>
|
||||||
return failure(serverNotInitializedError);
|
return failure(serverNotInitializedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
final pos = item.selectionRange.start;
|
|
||||||
final path = pathOfUri(item.uri);
|
final path = pathOfUri(item.uri);
|
||||||
final unit = await path.mapResult(requireResolvedUnit);
|
final unit = await path.mapResult(requireResolvedUnit);
|
||||||
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
|
final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
|
||||||
return offset.mapResult((offset) async {
|
final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
|
||||||
final supportedSymbolKinds = clientCapabilities.documentSymbolKinds;
|
|
||||||
final computer = call_hierarchy.DartCallHierarchyComputer(unit.result);
|
|
||||||
|
|
||||||
// Convert the clients item back to one in the servers format so that we
|
// Convert the clients item back to one in the servers format so that we
|
||||||
// can use it to get incoming/outgoing calls.
|
// can use it to get incoming/outgoing calls.
|
||||||
final target = toServerItem(
|
final target = toServerItem(
|
||||||
item,
|
item,
|
||||||
unit.result.lineInfo,
|
unit.result.lineInfo,
|
||||||
supportedSymbolKinds: supportedSymbolKinds,
|
supportedSymbolKinds: supportedSymbolKinds,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
return error(
|
||||||
|
ErrorCodes.ContentModified,
|
||||||
|
'Content was modified since Call Hierarchy node was produced',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (target == null) {
|
final calls = await getCalls(computer, target);
|
||||||
return error(
|
final results = _convertCalls(
|
||||||
ErrorCodes.ContentModified,
|
unit.result,
|
||||||
'Content was modified since Call Hierarchy node was produced',
|
calls,
|
||||||
);
|
supportedSymbolKinds,
|
||||||
}
|
);
|
||||||
|
return success(results);
|
||||||
final calls = await getCalls(computer, target);
|
|
||||||
final results = _convertCalls(
|
|
||||||
unit.result,
|
|
||||||
calls,
|
|
||||||
supportedSymbolKinds,
|
|
||||||
);
|
|
||||||
return success(results);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a server [call_hierarchy.CallHierarchyCalls] to the appropriate
|
/// Converts a server [call_hierarchy.CallHierarchyCalls] to the appropriate
|
||||||
|
|
|
@ -38,6 +38,7 @@ 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_signature_help.dart';
|
||||||
import 'package:analysis_server/src/lsp/handlers/handler_text_document_changes.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_type_definition.dart';
|
||||||
|
import 'package:analysis_server/src/lsp/handlers/handler_type_hierarchy.dart';
|
||||||
import 'package:analysis_server/src/lsp/handlers/handler_will_rename_files.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_configuration.dart';
|
||||||
import 'package:analysis_server/src/lsp/handlers/handler_workspace_symbols.dart';
|
import 'package:analysis_server/src/lsp/handlers/handler_workspace_symbols.dart';
|
||||||
|
@ -102,6 +103,9 @@ class InitializedStateMessageHandler extends ServerStateMessageHandler {
|
||||||
registerHandler(PrepareCallHierarchyHandler(server));
|
registerHandler(PrepareCallHierarchyHandler(server));
|
||||||
registerHandler(IncomingCallHierarchyHandler(server));
|
registerHandler(IncomingCallHierarchyHandler(server));
|
||||||
registerHandler(OutgoingCallHierarchyHandler(server));
|
registerHandler(OutgoingCallHierarchyHandler(server));
|
||||||
|
registerHandler(PrepareTypeHierarchyHandler(server));
|
||||||
|
registerHandler(TypeHierarchySubtypesHandler(server));
|
||||||
|
registerHandler(TypeHierarchySupertypesHandler(server));
|
||||||
registerHandler(FoldingHandler(server));
|
registerHandler(FoldingHandler(server));
|
||||||
registerHandler(DiagnosticServerHandler(server));
|
registerHandler(DiagnosticServerHandler(server));
|
||||||
registerHandler(WorkspaceSymbolHandler(server));
|
registerHandler(WorkspaceSymbolHandler(server));
|
||||||
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
// 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 'dart:async';
|
||||||
|
|
||||||
|
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_lazy_type_hierarchy.dart'
|
||||||
|
as type_hierarchy;
|
||||||
|
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
|
||||||
|
import 'package:analysis_server/src/lsp/mapping.dart';
|
||||||
|
import 'package:analyzer/dart/analysis/results.dart';
|
||||||
|
import 'package:analyzer/dart/analysis/session.dart';
|
||||||
|
import 'package:analyzer/source/line_info.dart';
|
||||||
|
import 'package:analyzer/source/source_range.dart';
|
||||||
|
|
||||||
|
/// A handler for the initial "prepare" request for starting navigation with
|
||||||
|
/// Type Hierarchy.
|
||||||
|
///
|
||||||
|
/// This handler returns the initial target based on the offset where the
|
||||||
|
/// feature is invoked. Invocations at item sites will resolve to the respective
|
||||||
|
/// declarations.
|
||||||
|
///
|
||||||
|
/// The target returned by this handler will be sent back to the server for
|
||||||
|
/// supertype/supertype items as the user navigates the type hierarchy in the
|
||||||
|
/// client.
|
||||||
|
class PrepareTypeHierarchyHandler extends MessageHandler<
|
||||||
|
TypeHierarchyPrepareParams,
|
||||||
|
TextDocumentPrepareTypeHierarchyResult> with _TypeHierarchyUtils {
|
||||||
|
PrepareTypeHierarchyHandler(super.server);
|
||||||
|
@override
|
||||||
|
Method get handlesMessage => Method.textDocument_prepareTypeHierarchy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
LspJsonHandler<TypeHierarchyPrepareParams> get jsonHandler =>
|
||||||
|
TypeHierarchyPrepareParams.jsonHandler;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ErrorOr<TextDocumentPrepareTypeHierarchyResult>> handle(
|
||||||
|
TypeHierarchyPrepareParams params,
|
||||||
|
MessageInfo message,
|
||||||
|
CancellationToken token) async {
|
||||||
|
if (!isDartDocument(params.textDocument)) {
|
||||||
|
return success(const []);
|
||||||
|
}
|
||||||
|
|
||||||
|
final clientCapabilities = server.clientCapabilities;
|
||||||
|
if (clientCapabilities == null) {
|
||||||
|
// This should not happen unless a client misbehaves.
|
||||||
|
return serverNotInitializedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pos = params.position;
|
||||||
|
final path = pathOfDoc(params.textDocument);
|
||||||
|
final unit = await path.mapResult(requireResolvedUnit);
|
||||||
|
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
|
||||||
|
return offset.mapResult((offset) {
|
||||||
|
final computer =
|
||||||
|
type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
|
||||||
|
final target = computer.findTarget(offset);
|
||||||
|
if (target == null) {
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final item = toLspItem(target, unit.result.lineInfo);
|
||||||
|
return success([item]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeHierarchySubtypesHandler extends MessageHandler<
|
||||||
|
TypeHierarchySubtypesParams,
|
||||||
|
TypeHierarchySubtypesResult> with _TypeHierarchyUtils {
|
||||||
|
TypeHierarchySubtypesHandler(super.server);
|
||||||
|
@override
|
||||||
|
Method get handlesMessage => Method.typeHierarchy_subtypes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
LspJsonHandler<TypeHierarchySubtypesParams> get jsonHandler =>
|
||||||
|
TypeHierarchySubtypesParams.jsonHandler;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ErrorOr<TypeHierarchySubtypesResult>> handle(
|
||||||
|
TypeHierarchySubtypesParams params,
|
||||||
|
MessageInfo message,
|
||||||
|
CancellationToken token) async {
|
||||||
|
final item = params.item;
|
||||||
|
final path = pathOfUri(item.uri);
|
||||||
|
final unit = await path.mapResult(requireResolvedUnit);
|
||||||
|
final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
|
||||||
|
|
||||||
|
// Convert the clients item back to one in the servers format so that we
|
||||||
|
// can use it to get sub/super types.
|
||||||
|
final target = toServerItem(item, unit.result.lineInfo);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
return error(
|
||||||
|
ErrorCodes.ContentModified,
|
||||||
|
'Content was modified since Type Hierarchy node was produced',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final calls = await computer.findSubtypes(target, server.searchEngine);
|
||||||
|
final results = calls != null ? _convertItems(unit.result, calls) : null;
|
||||||
|
return success(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeHierarchySupertypesHandler extends MessageHandler<
|
||||||
|
TypeHierarchySupertypesParams,
|
||||||
|
TypeHierarchySupertypesResult> with _TypeHierarchyUtils {
|
||||||
|
TypeHierarchySupertypesHandler(super.server);
|
||||||
|
@override
|
||||||
|
Method get handlesMessage => Method.typeHierarchy_supertypes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
LspJsonHandler<TypeHierarchySupertypesParams> get jsonHandler =>
|
||||||
|
TypeHierarchySupertypesParams.jsonHandler;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ErrorOr<TypeHierarchySupertypesResult>> handle(
|
||||||
|
TypeHierarchySupertypesParams params,
|
||||||
|
MessageInfo message,
|
||||||
|
CancellationToken token) async {
|
||||||
|
final item = params.item;
|
||||||
|
final path = pathOfUri(item.uri);
|
||||||
|
final unit = await path.mapResult(requireResolvedUnit);
|
||||||
|
final computer = type_hierarchy.DartLazyTypeHierarchyComputer(unit.result);
|
||||||
|
|
||||||
|
// Convert the clients item back to one in the servers format so that we
|
||||||
|
// can use it to get sub/super types.
|
||||||
|
final target = toServerItem(item, unit.result.lineInfo);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
return error(
|
||||||
|
ErrorCodes.ContentModified,
|
||||||
|
'Content was modified since Type Hierarchy node was produced',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final calls = await computer.findSupertypes(target);
|
||||||
|
final results = calls != null ? _convertItems(unit.result, calls) : null;
|
||||||
|
return success(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility methods used by all Type Hierarchy handlers.
|
||||||
|
mixin _TypeHierarchyUtils {
|
||||||
|
/// Converts a server [SourceRange] to an LSP [Range].
|
||||||
|
Range sourceRangeToRange(LineInfo lineInfo, SourceRange range) =>
|
||||||
|
toRange(lineInfo, range.offset, range.length);
|
||||||
|
|
||||||
|
/// Converts a server [type_hierarchy.TypeHierarchyItem] to an LSP
|
||||||
|
/// [TypeHierarchyItem].
|
||||||
|
TypeHierarchyItem toLspItem(
|
||||||
|
type_hierarchy.TypeHierarchyItem item,
|
||||||
|
LineInfo lineInfo,
|
||||||
|
) {
|
||||||
|
return TypeHierarchyItem(
|
||||||
|
name: item.displayName,
|
||||||
|
detail: _detailFor(item),
|
||||||
|
kind: SymbolKind.Class,
|
||||||
|
uri: Uri.file(item.file),
|
||||||
|
range: sourceRangeToRange(lineInfo, item.codeRange),
|
||||||
|
selectionRange: sourceRangeToRange(lineInfo, item.nameRange),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an LSP [TypeHierarchyItem] supplied by the client back to a
|
||||||
|
/// server [type_hierarchy.TypeHierarchyItem] to use to look up items.
|
||||||
|
///
|
||||||
|
/// Returns `null` if the supplied item is no longer valid (for example its
|
||||||
|
/// ranges are no longer valid in the current state of the document).
|
||||||
|
type_hierarchy.TypeHierarchyItem? toServerItem(
|
||||||
|
TypeHierarchyItem item,
|
||||||
|
LineInfo lineInfo,
|
||||||
|
) {
|
||||||
|
final nameRange = toSourceRange(lineInfo, item.selectionRange);
|
||||||
|
final codeRange = toSourceRange(lineInfo, item.range);
|
||||||
|
if (nameRange.isError || codeRange.isError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type_hierarchy.TypeHierarchyItem(
|
||||||
|
displayName: item.name,
|
||||||
|
file: item.uri.toFilePath(),
|
||||||
|
nameRange: nameRange.result,
|
||||||
|
codeRange: codeRange.result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a server [type_hierarchy.TypeHierarchyItem] to an LSP
|
||||||
|
/// [TypeHierarchyItem].
|
||||||
|
///
|
||||||
|
/// Reads [LineInfo]s from [session], using [lineInfoCache] as a cache.
|
||||||
|
TypeHierarchyItem? _convertItem(
|
||||||
|
AnalysisSession session,
|
||||||
|
Map<String, LineInfo?> lineInfoCache,
|
||||||
|
type_hierarchy.TypeHierarchyItem item,
|
||||||
|
) {
|
||||||
|
final filePath = item.file;
|
||||||
|
final lineInfo = lineInfoCache.putIfAbsent(filePath, () {
|
||||||
|
final file = session.getFile(filePath);
|
||||||
|
return file is FileResult ? file.lineInfo : null;
|
||||||
|
});
|
||||||
|
if (lineInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toLspItem(item, lineInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts multiple server [type_hierarchy.TypeHierarchyItem] to an LSP
|
||||||
|
/// [TypeHierarchyItem].
|
||||||
|
///
|
||||||
|
/// Reads [LineInfo]s from [unit.session], caching them for items in the same
|
||||||
|
/// file.
|
||||||
|
List<TypeHierarchyItem> _convertItems(
|
||||||
|
ResolvedUnitResult unit,
|
||||||
|
List<type_hierarchy.TypeHierarchyRelatedItem> items,
|
||||||
|
) {
|
||||||
|
final session = unit.session;
|
||||||
|
final lineInfoCache = <String, LineInfo?>{
|
||||||
|
unit.path: unit.lineInfo,
|
||||||
|
};
|
||||||
|
final results = convert(
|
||||||
|
items,
|
||||||
|
(type_hierarchy.TypeHierarchyRelatedItem item) => _convertItem(
|
||||||
|
session,
|
||||||
|
lineInfoCache,
|
||||||
|
item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return results.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the "detail" label for [item].
|
||||||
|
///
|
||||||
|
/// This includes a user-visible description of the relationship between the
|
||||||
|
/// target item and [item].
|
||||||
|
String? _detailFor(type_hierarchy.TypeHierarchyItem item) {
|
||||||
|
if (item is! type_hierarchy.TypeHierarchyRelatedItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (item.relationship) {
|
||||||
|
case type_hierarchy.TypeHierarchyItemRelationship.extends_:
|
||||||
|
return 'extends';
|
||||||
|
case type_hierarchy.TypeHierarchyItemRelationship.implements:
|
||||||
|
return 'implements';
|
||||||
|
case type_hierarchy.TypeHierarchyItemRelationship.mixesIn:
|
||||||
|
return 'mixes in';
|
||||||
|
case type_hierarchy.TypeHierarchyItemRelationship.constrainedTo:
|
||||||
|
return 'constrained to';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ class ClientDynamicRegistrations {
|
||||||
Method.textDocument_selectionRange,
|
Method.textDocument_selectionRange,
|
||||||
Method.textDocument_typeDefinition,
|
Method.textDocument_typeDefinition,
|
||||||
Method.textDocument_prepareCallHierarchy,
|
Method.textDocument_prepareCallHierarchy,
|
||||||
|
Method.textDocument_prepareTypeHierarchy,
|
||||||
// workspace.fileOperations covers all file operation methods but we only
|
// workspace.fileOperations covers all file operation methods but we only
|
||||||
// support this one.
|
// support this one.
|
||||||
Method.workspace_willRenameFiles,
|
Method.workspace_willRenameFiles,
|
||||||
|
@ -123,6 +124,9 @@ class ClientDynamicRegistrations {
|
||||||
bool get typeFormatting =>
|
bool get typeFormatting =>
|
||||||
_capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
|
_capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
|
||||||
false;
|
false;
|
||||||
|
|
||||||
|
bool get typeHierarchy =>
|
||||||
|
_capabilities.textDocument?.typeHierarchy?.dynamicRegistration ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerCapabilitiesComputer {
|
class ServerCapabilitiesComputer {
|
||||||
|
@ -296,6 +300,10 @@ class ServerCapabilitiesComputer {
|
||||||
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
|
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
typeHierarchyProvider: dynamicRegistrations.typeHierarchy
|
||||||
|
? null
|
||||||
|
: Either3<bool, TypeHierarchyOptions,
|
||||||
|
TypeHierarchyRegistrationOptions>.t1(true),
|
||||||
executeCommandProvider: ExecuteCommandOptions(
|
executeCommandProvider: ExecuteCommandOptions(
|
||||||
commands: Commands.serverSupportedCommands,
|
commands: Commands.serverSupportedCommands,
|
||||||
workDoneProgress: true,
|
workDoneProgress: true,
|
||||||
|
@ -546,6 +554,13 @@ class ServerCapabilitiesComputer {
|
||||||
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
|
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
register(
|
||||||
|
dynamicRegistrations.typeHierarchy,
|
||||||
|
Method.textDocument_prepareTypeHierarchy,
|
||||||
|
TypeHierarchyRegistrationOptions(
|
||||||
|
documentSelector: [dartFiles],
|
||||||
|
),
|
||||||
|
);
|
||||||
register(
|
register(
|
||||||
dynamicRegistrations.inlayHints,
|
dynamicRegistrations.inlayHints,
|
||||||
Method.textDocument_inlayHint,
|
Method.textDocument_inlayHint,
|
||||||
|
|
|
@ -41,7 +41,9 @@ class InitializationTest extends AbstractLspAnalysisServerTest {
|
||||||
) {
|
) {
|
||||||
final options = registrationFor(registrations, method)?.registerOptions;
|
final options = registrationFor(registrations, method)?.registerOptions;
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
throw 'Registration options for $method were not found';
|
throw 'Registration options for $method were not found. '
|
||||||
|
'Perhaps dynamicRegistration is missing from '
|
||||||
|
'withAllSupportedTextDocumentDynamicRegistrations?';
|
||||||
}
|
}
|
||||||
return TextDocumentRegistrationOptions.fromJson(
|
return TextDocumentRegistrationOptions.fromJson(
|
||||||
options as Map<String, Object?>);
|
options as Map<String, Object?>);
|
||||||
|
|
|
@ -325,6 +325,7 @@ mixin ClientCapabilitiesHelperMixin {
|
||||||
tokenModifiers: [],
|
tokenModifiers: [],
|
||||||
tokenTypes: []).toJson(),
|
tokenTypes: []).toJson(),
|
||||||
'typeDefinition': {'dynamicRegistration': true},
|
'typeDefinition': {'dynamicRegistration': true},
|
||||||
|
'typeHierarchy': {'dynamicRegistration': true},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1718,6 +1719,18 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
|
||||||
return expectSuccessfulResponseTo(request, PlaceholderAndRange.fromJson);
|
return expectSuccessfulResponseTo(request, PlaceholderAndRange.fromJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<TypeHierarchyItem>?> prepareTypeHierarchy(Uri uri, Position pos) {
|
||||||
|
final request = makeRequest(
|
||||||
|
Method.textDocument_prepareTypeHierarchy,
|
||||||
|
TypeHierarchyPrepareParams(
|
||||||
|
textDocument: TextDocumentIdentifier(uri: uri),
|
||||||
|
position: pos,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return expectSuccessfulResponseTo(
|
||||||
|
request, _fromJsonList(TypeHierarchyItem.fromJson));
|
||||||
|
}
|
||||||
|
|
||||||
/// Calls the supplied function and responds to any `workspace/configuration`
|
/// Calls the supplied function and responds to any `workspace/configuration`
|
||||||
/// request with the supplied config.
|
/// request with the supplied config.
|
||||||
Future<T> provideConfig<T>(
|
Future<T> provideConfig<T>(
|
||||||
|
@ -1932,6 +1945,26 @@ mixin LspAnalysisServerTestMixin implements ClientCapabilitiesHelperMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<TypeHierarchyItem>?> typeHierarchySubtypes(
|
||||||
|
TypeHierarchyItem item) {
|
||||||
|
final request = makeRequest(
|
||||||
|
Method.typeHierarchy_subtypes,
|
||||||
|
TypeHierarchySubtypesParams(item: item),
|
||||||
|
);
|
||||||
|
return expectSuccessfulResponseTo(
|
||||||
|
request, _fromJsonList(TypeHierarchyItem.fromJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TypeHierarchyItem>?> typeHierarchySupertypes(
|
||||||
|
TypeHierarchyItem item) {
|
||||||
|
final request = makeRequest(
|
||||||
|
Method.typeHierarchy_supertypes,
|
||||||
|
TypeHierarchySupertypesParams(item: item),
|
||||||
|
);
|
||||||
|
return expectSuccessfulResponseTo(
|
||||||
|
request, _fromJsonList(TypeHierarchyItem.fromJson));
|
||||||
|
}
|
||||||
|
|
||||||
/// Tells the server the config has changed, and provides the supplied config
|
/// Tells the server the config has changed, and provides the supplied config
|
||||||
/// when it requests the updated config.
|
/// when it requests the updated config.
|
||||||
Future<ResponseMessage> updateConfig(Map<String, dynamic> config) {
|
Future<ResponseMessage> updateConfig(Map<String, dynamic> config) {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import 'signature_help_test.dart' as signature_help;
|
||||||
import 'snippets_test.dart' as snippets;
|
import 'snippets_test.dart' as snippets;
|
||||||
import 'super_test.dart' as get_super;
|
import 'super_test.dart' as get_super;
|
||||||
import 'type_definition_test.dart' as type_definition;
|
import 'type_definition_test.dart' as type_definition;
|
||||||
|
import 'type_hierarchy_test.dart' as type_hierarchy;
|
||||||
import 'will_rename_files_test.dart' as will_rename_files;
|
import 'will_rename_files_test.dart' as will_rename_files;
|
||||||
import 'workspace_symbols_test.dart' as workspace_symbols;
|
import 'workspace_symbols_test.dart' as workspace_symbols;
|
||||||
|
|
||||||
|
@ -93,6 +94,7 @@ void main() {
|
||||||
signature_help.main();
|
signature_help.main();
|
||||||
snippets.main();
|
snippets.main();
|
||||||
type_definition.main();
|
type_definition.main();
|
||||||
|
type_hierarchy.main();
|
||||||
will_rename_files.main();
|
will_rename_files.main();
|
||||||
workspace_symbols.main();
|
workspace_symbols.main();
|
||||||
}, name: 'lsp');
|
}, name: 'lsp');
|
||||||
|
|
359
pkg/analysis_server/test/lsp/type_hierarchy_test.dart
Normal file
359
pkg/analysis_server/test/lsp/type_hierarchy_test.dart
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
// 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:collection/collection.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||||
|
|
||||||
|
import '../utils/test_code_format.dart';
|
||||||
|
import 'server_abstract.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
defineReflectiveSuite(() {
|
||||||
|
// These tests cover the LSP handler. A complete set of Type Hierarchy tests
|
||||||
|
// are in 'test/src/computer/type_hierarchy_computer_test.dart'.
|
||||||
|
defineReflectiveTests(PrepareTypeHierarchyTest);
|
||||||
|
defineReflectiveTests(TypeHierarchySupertypesTest);
|
||||||
|
defineReflectiveTests(TypeHierarchySubtypesTest);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AbstractTypeHierarchyTest extends AbstractLspAnalysisServerTest {
|
||||||
|
/// Code being tested in the main file.
|
||||||
|
late TestCode code;
|
||||||
|
|
||||||
|
/// Another file for testing cross-file content.
|
||||||
|
late final String otherFilePath;
|
||||||
|
late final Uri otherFileUri;
|
||||||
|
late TestCode otherCode;
|
||||||
|
|
||||||
|
/// The result of the last prepareTypeHierarchy call.
|
||||||
|
TypeHierarchyItem? prepareResult;
|
||||||
|
|
||||||
|
late final dartCodeUri = Uri.file(convertPath('/sdk/lib/core/core.dart'));
|
||||||
|
|
||||||
|
/// Matches a [TypeHierarchyItem] for [Object] with an 'extends' relationship.
|
||||||
|
Matcher get _isExtendsObject => TypeMatcher<TypeHierarchyItem>()
|
||||||
|
.having((e) => e.name, 'name', 'Object')
|
||||||
|
.having((e) => e.uri, 'uri', dartCodeUri)
|
||||||
|
.having((e) => e.kind, 'kind', SymbolKind.Class)
|
||||||
|
.having((e) => e.detail, 'detail', 'extends')
|
||||||
|
.having((e) => e.selectionRange, 'selectionRange', _isValidRange)
|
||||||
|
.having((e) => e.range, 'range', _isValidRange);
|
||||||
|
|
||||||
|
/// Matches a valid [Position].
|
||||||
|
Matcher get _isValidPosition => TypeMatcher<Position>()
|
||||||
|
.having((e) => e.line, 'line', greaterThanOrEqualTo(0))
|
||||||
|
.having((e) => e.character, 'character', greaterThanOrEqualTo(0));
|
||||||
|
|
||||||
|
/// Matches a [Range] with valid [Position]s.
|
||||||
|
Matcher get _isValidRange => TypeMatcher<Range>()
|
||||||
|
.having((e) => e.start, 'start', _isValidPosition)
|
||||||
|
.having((e) => e.end, 'end', _isValidPosition);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setUp() {
|
||||||
|
super.setUp();
|
||||||
|
otherFilePath = join(projectFolderPath, 'lib', 'other.dart');
|
||||||
|
otherFileUri = Uri.file(otherFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches a [TypeHierarchyItem] with the given values.
|
||||||
|
Matcher _isItem(
|
||||||
|
String name,
|
||||||
|
Uri uri, {
|
||||||
|
String? detail,
|
||||||
|
required Range selectionRange,
|
||||||
|
required Range range,
|
||||||
|
}) =>
|
||||||
|
TypeMatcher<TypeHierarchyItem>()
|
||||||
|
.having((e) => e.name, 'name', name)
|
||||||
|
.having((e) => e.uri, 'uri', uri)
|
||||||
|
.having((e) => e.kind, 'kind', SymbolKind.Class)
|
||||||
|
.having((e) => e.detail, 'detail', detail)
|
||||||
|
.having((e) => e.selectionRange, 'selectionRange', selectionRange)
|
||||||
|
.having((e) => e.range, 'range', range);
|
||||||
|
|
||||||
|
/// Parses [content] and calls 'textDocument/prepareTypeHierarchy' at the
|
||||||
|
/// marked location.
|
||||||
|
Future<void> _prepareTypeHierarchy(String content,
|
||||||
|
{String? otherContent}) async {
|
||||||
|
code = TestCode.parse(content);
|
||||||
|
newFile(mainFilePath, code.code);
|
||||||
|
|
||||||
|
if (otherContent != null) {
|
||||||
|
otherCode = TestCode.parse(otherContent);
|
||||||
|
newFile(otherFilePath, otherCode.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initialize();
|
||||||
|
final result = await prepareTypeHierarchy(
|
||||||
|
mainFileUri,
|
||||||
|
code.position.lsp,
|
||||||
|
);
|
||||||
|
prepareResult = result?.singleOrNull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@reflectiveTest
|
||||||
|
class PrepareTypeHierarchyTest extends AbstractTypeHierarchyTest {
|
||||||
|
Future<void> test_class() async {
|
||||||
|
final content = '''
|
||||||
|
/*[0*/class /*[1*/MyC^lass1/*1]*/ {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _prepareTypeHierarchy(content);
|
||||||
|
expect(
|
||||||
|
prepareResult,
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
mainFileUri,
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_nonClass() async {
|
||||||
|
final content = '''
|
||||||
|
int? a^a;
|
||||||
|
''';
|
||||||
|
await _prepareTypeHierarchy(content);
|
||||||
|
expect(prepareResult, isNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_whitespace() async {
|
||||||
|
final content = '''
|
||||||
|
int? a;
|
||||||
|
^
|
||||||
|
int? b;
|
||||||
|
''';
|
||||||
|
await _prepareTypeHierarchy(content);
|
||||||
|
expect(prepareResult, isNull);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@reflectiveTest
|
||||||
|
class TypeHierarchySubtypesTest extends AbstractTypeHierarchyTest {
|
||||||
|
List<TypeHierarchyItem>? subtypes;
|
||||||
|
|
||||||
|
Future<void> test_anotherFile() async {
|
||||||
|
final content = '''
|
||||||
|
class MyCl^ass1 {}
|
||||||
|
''';
|
||||||
|
final otherContent = '''
|
||||||
|
import 'main.dart';
|
||||||
|
|
||||||
|
/*[0*/class /*[1*/MyClass2/*1]*/ extends MyClass1 {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSubtypes(content, otherContent: otherContent);
|
||||||
|
expect(
|
||||||
|
subtypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass2',
|
||||||
|
otherFileUri,
|
||||||
|
detail: 'extends',
|
||||||
|
range: otherCode.ranges[0].lsp,
|
||||||
|
selectionRange: otherCode.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_extends() async {
|
||||||
|
final content = '''
|
||||||
|
class MyCla^ss1 {}
|
||||||
|
/*[0*/class /*[1*/MyClass2/*1]*/ extends MyClass1 {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSubtypes(content);
|
||||||
|
expect(
|
||||||
|
subtypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass2',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'extends',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_implements() async {
|
||||||
|
final content = '''
|
||||||
|
class MyCla^ss1 {}
|
||||||
|
/*[0*/class /*[1*/MyClass2/*1]*/ implements MyClass1 {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSubtypes(content);
|
||||||
|
expect(
|
||||||
|
subtypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass2',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'implements',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_on() async {
|
||||||
|
final content = '''
|
||||||
|
class MyCla^ss1 {}
|
||||||
|
/*[0*/mixin /*[1*/MyMixin1/*1]*/ on MyClass1 {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSubtypes(content);
|
||||||
|
expect(
|
||||||
|
subtypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyMixin1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'constrained to',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_with() async {
|
||||||
|
final content = '''
|
||||||
|
mixin MyMi^xin1 {}
|
||||||
|
/*[0*/class /*[1*/MyClass1/*1]*/ with MyMixin1 {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSubtypes(content);
|
||||||
|
expect(
|
||||||
|
subtypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'mixes in',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses [content], calls 'textDocument/prepareTypeHierarchy' at the
|
||||||
|
/// marked location and then calls 'typeHierarchy/subtypes' with the result.
|
||||||
|
Future<void> _fetchSubtypes(String content, {String? otherContent}) async {
|
||||||
|
await _prepareTypeHierarchy(content, otherContent: otherContent);
|
||||||
|
subtypes = await typeHierarchySubtypes(prepareResult!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@reflectiveTest
|
||||||
|
class TypeHierarchySupertypesTest extends AbstractTypeHierarchyTest {
|
||||||
|
List<TypeHierarchyItem>? supertypes;
|
||||||
|
|
||||||
|
Future<void> test_anotherFile() async {
|
||||||
|
final content = '''
|
||||||
|
import 'other.dart';
|
||||||
|
|
||||||
|
class MyCla^ss2 extends MyClass1 {}
|
||||||
|
''';
|
||||||
|
final otherContent = '''
|
||||||
|
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||||
|
''';
|
||||||
|
await _fetchSupertypes(content, otherContent: otherContent);
|
||||||
|
expect(
|
||||||
|
supertypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
otherFileUri,
|
||||||
|
detail: 'extends',
|
||||||
|
range: otherCode.ranges[0].lsp,
|
||||||
|
selectionRange: otherCode.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_extends() async {
|
||||||
|
final content = '''
|
||||||
|
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||||
|
class MyCla^ss2 extends MyClass1 {}
|
||||||
|
''';
|
||||||
|
await _fetchSupertypes(content);
|
||||||
|
expect(
|
||||||
|
supertypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'extends',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_implements() async {
|
||||||
|
final content = '''
|
||||||
|
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||||
|
class MyCla^ss2 implements MyClass1 {}
|
||||||
|
''';
|
||||||
|
await _fetchSupertypes(content);
|
||||||
|
expect(
|
||||||
|
supertypes,
|
||||||
|
equals([
|
||||||
|
_isExtendsObject,
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'implements',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_on() async {
|
||||||
|
final content = '''
|
||||||
|
/*[0*/class /*[1*/MyClass1/*1]*/ {}/*0]*/
|
||||||
|
mixin MyMix^in1 on MyClass1 {}
|
||||||
|
''';
|
||||||
|
await _fetchSupertypes(content);
|
||||||
|
expect(
|
||||||
|
supertypes,
|
||||||
|
equals([
|
||||||
|
_isItem(
|
||||||
|
'MyClass1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'constrained to',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> test_with() async {
|
||||||
|
final content = '''
|
||||||
|
/*[0*/mixin /*[1*/MyMixin1/*1]*/ {}/*0]*/
|
||||||
|
class MyCla^ss1 with MyMixin1 {}
|
||||||
|
''';
|
||||||
|
await _fetchSupertypes(content);
|
||||||
|
expect(
|
||||||
|
supertypes,
|
||||||
|
equals([
|
||||||
|
_isExtendsObject,
|
||||||
|
_isItem(
|
||||||
|
'MyMixin1',
|
||||||
|
mainFileUri,
|
||||||
|
detail: 'mixes in',
|
||||||
|
range: code.ranges[0].lsp,
|
||||||
|
selectionRange: code.ranges[1].lsp,
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses [content], calls 'textDocument/prepareTypeHierarchy' at the
|
||||||
|
/// marked location and then calls 'typeHierarchy/supertypes' with the result.
|
||||||
|
Future<void> _fetchSupertypes(String content, {String? otherContent}) async {
|
||||||
|
await _prepareTypeHierarchy(content, otherContent: otherContent);
|
||||||
|
supertypes = await typeHierarchySupertypes(prepareResult!);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue