blob: c6549d1303c077ff8c4d319b52f2549c19f2e7a0 [file] [log] [blame]
// Copyright (c) 2018, 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 Element;
import 'package:analysis_server/protocol/protocol_generated.dart'
hide AnalysisGetNavigationParams;
import 'package:analysis_server/src/lsp/error_or.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/plugin/result_merger.dart';
import 'package:analysis_server/src/protocol_server.dart' show NavigationTarget;
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:analyzer_plugin/utilities/analyzer_converter.dart';
import 'package:analyzer_plugin/utilities/navigation/navigation_dart.dart';
typedef StaticOptions = Either2<bool, DefinitionOptions>;
class DefinitionHandler extends LspMessageHandler<TextDocumentPositionParams,
TextDocumentDefinitionResult> with LspPluginRequestHandlerMixin {
DefinitionHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_definition;
@override
LspJsonHandler<TextDocumentPositionParams> get jsonHandler =>
TextDocumentPositionParams.jsonHandler;
Future<List<AnalysisNavigationParams>> getPluginResults(
String path,
int offset,
) async {
// LSP requests must be converted to DAS-protocol requests for compatibility
// with plugins.
var requestParams = plugin.AnalysisGetNavigationParams(path, offset, 0);
var responses = await requestFromPlugins(path, requestParams);
return responses
.map((response) =>
plugin.AnalysisGetNavigationResult.fromResponse(response))
.map((result) => AnalysisNavigationParams(
path, result.regions, result.targets, result.files))
.toList();
}
Future<AnalysisNavigationParams> getServerResult(ResolvedUnitResult result,
String path, bool supportsLocationLink, int offset) async {
var collector = NavigationCollectorImpl();
computeDartNavigation(
server.resourceProvider, collector, result, offset, 0);
if (supportsLocationLink) {
await _updateTargetsWithCodeLocations(collector);
}
collector.createRegions();
return AnalysisNavigationParams(
path, collector.regions, collector.targets, collector.files);
}
@override
Future<ErrorOr<TextDocumentDefinitionResult>> handle(
TextDocumentPositionParams params,
MessageInfo message,
CancellationToken token) async {
var clientCapabilities = message.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return serverNotInitializedError;
}
var supportsLocationLink = clientCapabilities.definitionLocationLink;
var pos = params.position;
var path = pathOfDoc(params.textDocument);
return path.mapResult((path) async {
// Always prefer a LineInfo from a resolved unit than server.getLineInfo.
var resolvedUnit = (await requireResolvedUnit(path)).resultOrNull;
var lineInfo = resolvedUnit?.lineInfo ?? server.getLineInfo(path);
// If there is no lineInfo, the request cannot be translated from LSP line/col
// to server offset/length.
if (lineInfo == null) {
return success(TextDocumentDefinitionResult.t2(const []));
}
var offset = toOffset(lineInfo, pos);
return offset.mapResult((offset) async {
var allResults = [
if (resolvedUnit != null)
await getServerResult(
resolvedUnit, path, supportsLocationLink, offset),
...await getPluginResults(path, offset),
];
var merger = ResultMerger();
var mergedResults = merger.mergeNavigation(allResults);
var mergedTargets = mergedResults?.targets ?? [];
if (mergedResults == null) {
return success(TextDocumentDefinitionResult.t2(const []));
}
// Convert and filter the results using the correct type of Location class
// depending on the client capabilities.
if (supportsLocationLink) {
var convertedResults = convert(
mergedTargets,
(NavigationTarget target) =>
_toLocationLink(mergedResults, lineInfo, target),
).nonNulls.toList();
var results = _filterResults(
convertedResults,
params.textDocument.uri,
pos.line,
(LocationLink element) => element.targetUri,
(LocationLink element) => element.targetSelectionRange,
);
return success(TextDocumentDefinitionResult.t2(results));
} else {
var convertedResults = convert(
mergedTargets,
(NavigationTarget target) => _toLocation(mergedResults, target),
).nonNulls.toList();
var results = _filterResults(
convertedResults,
params.textDocument.uri,
pos.line,
(Location element) => element.uri,
(Location element) => element.range,
);
return success(
TextDocumentDefinitionResult.t1(Definition.t1(results)),
);
}
});
});
}
/// Helper that selects the correct results (filtering out at the same
/// line/location) generically, handling either type of Location class.
List<T> _filterResults<T>(
List<T> results,
Uri sourceUri,
int sourceLineNumber,
Uri Function(T) uriSelector,
Range Function(T) rangeSelector,
) {
// If we fetch navigation on a keyword like `var`, the results will include
// both the definition and also the variable name. This will cause the editor
// to show the user both options unnecessarily (the variable name is always
// adjacent to the var keyword, so providing navigation to it is not useful).
// To prevent this, filter the list to only those on different lines (or
// different files).
var otherResults = results
.where((element) =>
uriSelector(element) != sourceUri ||
rangeSelector(element).start.line != sourceLineNumber)
.toList();
return otherResults.isNotEmpty ? otherResults : results;
}
/// Get the location of the code (excluding leading doc comments) for this element.
Future<protocol.Location?> _getCodeLocation(Element element) async {
Element? codeElement = element;
// For synthetic getters created for fields, we need to access the associated
// variable to get the codeOffset/codeLength.
if (codeElement is PropertyAccessorElementImpl && codeElement.isSynthetic) {
codeElement = codeElement.variable2!;
}
// For extension types, the primary constructor has a range that covers only
// the parameters / representation type but we want the whole declaration
// for the code range because otherwise previews will just show `(int a)`
// which is not what the user expects.
if (codeElement.enclosingElement3 case ExtensionTypeElement enclosingElement
when enclosingElement.primaryConstructor == codeElement) {
codeElement = enclosingElement;
}
// Read the main codeOffset from the element. This may include doc comments
// but will give the correct end position.
int? codeOffset, codeLength;
if (codeElement is ElementImpl) {
codeOffset = codeElement.codeOffset;
codeLength = codeElement.codeLength;
}
if (codeOffset == null || codeLength == null) {
return null;
}
// Read the declaration so we can get the offset after the doc comments.
var declaration = await _parsedDeclaration(codeElement);
var node = declaration?.node;
if (node is VariableDeclaration) {
// For variables, expand to the variable declaration list if this is the
// only variable so that the target range can include keywords/type.
// Don't do this when there are multiple becaues it may include other
// variables in the range.
var parent = node.parent;
if (parent is VariableDeclarationList && parent.variables.length == 1) {
node = node.parent;
}
}
if (node is AnnotatedNode) {
var offsetAfterDocs = node.firstTokenAfterCommentAndMetadata.offset;
// Reduce the length by the difference between the end of docs and the start.
codeLength -= offsetAfterDocs - codeOffset;
codeOffset = offsetAfterDocs;
}
return AnalyzerConverter()
.locationFromElement(element, offset: codeOffset, length: codeLength);
}
Location? _toLocation(
AnalysisNavigationParams mergedResults, NavigationTarget target) {
var targetFilePath = mergedResults.files[target.fileIndex];
var targetFileUri = uriConverter.toClientUri(targetFilePath);
var targetLineInfo = server.getLineInfo(targetFilePath);
return targetLineInfo != null
? navigationTargetToLocation(targetFileUri, target, targetLineInfo)
: null;
}
LocationLink? _toLocationLink(AnalysisNavigationParams mergedResults,
LineInfo sourceLineInfo, NavigationTarget target) {
var region = mergedResults.regions.first;
var targetFilePath = mergedResults.files[target.fileIndex];
var targetFileUri = uriConverter.toClientUri(targetFilePath);
var targetLineInfo = server.getLineInfo(targetFilePath);
return targetLineInfo != null
? navigationTargetToLocationLink(
region, sourceLineInfo, targetFileUri, target, targetLineInfo)
: null;
}
Future<void> _updateTargetsWithCodeLocations(
NavigationCollectorImpl collector,
) async {
for (var targetToUpdate in collector.targetsToUpdate) {
var codeLocation = await _getCodeLocation(targetToUpdate.element);
if (codeLocation != null) {
targetToUpdate.target
..codeOffset = codeLocation.offset
..codeLength = codeLocation.length;
}
}
}
static Future<ElementDeclarationResult?> _parsedDeclaration(
Element element,
) async {
var session = element.session;
if (session == null) {
return null;
}
var libraryPath = element.library?.source.fullName;
if (libraryPath == null) {
return null;
}
var parsedLibrary = session.getParsedLibrary(libraryPath);
if (parsedLibrary is! ParsedLibraryResult) {
return null;
}
return parsedLibrary.getElementDeclaration(element);
}
}
class DefinitionRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
DefinitionRegistrations(super.info);
@override
ToJsonable? get options =>
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes);
@override
Method get registrationMethod => Method.textDocument_definition;
@override
StaticOptions get staticOptions => Either2.t1(true);
@override
bool get supportsDynamic => clientDynamic.definition;
}