blob: a6399a9c10e8f5ce2dc4934ffa25f42e997aa51f [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_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/protocol/protocol_generated.dart'
hide AnalysisGetNavigationParams;
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: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/source/line_info.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:analyzer_plugin/utilities/navigation/navigation_dart.dart';
class DefinitionHandler extends MessageHandler<TextDocumentPositionParams,
Either2<List<Location>, List<LocationLink>>>
with LspPluginRequestHandlerMixin {
DefinitionHandler(LspAnalysisServer server) : 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.
final requestParams = plugin.AnalysisGetNavigationParams(path, offset, 0);
final 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(
bool clientSupportsLocationLink, String path, int offset) async {
final collector = NavigationCollectorImpl(
collectCodeLocations: clientSupportsLocationLink);
final result = await server.getResolvedUnit(path);
if (result?.state == ResultState.VALID) {
computeDartNavigation(
server.resourceProvider, collector, result.unit, offset, 0);
collector.createRegions();
}
return AnalysisNavigationParams(
path, collector.regions, collector.targets, collector.files);
}
@override
Future<ErrorOr<Either2<List<Location>, List<LocationLink>>>> handle(
TextDocumentPositionParams params, CancellationToken token) async {
final definitionCapabilities =
server?.clientCapabilities?.textDocument?.definition;
final clientSupportsLocationLink =
definitionCapabilities?.linkSupport == true;
final pos = params.position;
final path = pathOfDoc(params.textDocument);
return path.mapResult((path) async {
final 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(
Either2<List<Location>, List<LocationLink>>.t1(const []),
);
}
final offset = toOffset(lineInfo, pos);
return offset.mapResult((offset) async {
final allResults = [
await getServerResult(clientSupportsLocationLink, path, offset),
...await getPluginResults(path, offset),
];
final merger = ResultMerger();
final mergedResults = merger.mergeNavigation(allResults);
final mergedTargets = mergedResults?.targets ?? [];
// Convert and filter the results using the correct type of Location class
// depending on the client capabilities.
if (clientSupportsLocationLink) {
final convertedResults = convert(
mergedTargets,
(target) => _toLocationLink(mergedResults, lineInfo, target),
).toList();
final results = _filterResults(
convertedResults,
params.textDocument.uri,
pos.line,
(element) => element.targetUri,
(element) => element.targetSelectionRange,
);
return success(
Either2<List<Location>, List<LocationLink>>.t2(results),
);
} else {
final convertedResults = convert(
mergedTargets,
(target) => _toLocation(mergedResults, target),
).toList();
final results = _filterResults(
convertedResults,
params.textDocument.uri,
pos.line,
(element) => element.uri,
(element) => element.range,
);
return success(
Either2<List<Location>, List<LocationLink>>.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,
String sourceUri,
int sourceLineNumber,
String 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).
final otherResults = results
.where((element) =>
uriSelector(element) != sourceUri ||
rangeSelector(element).start.line != sourceLineNumber)
.toList();
return otherResults.isNotEmpty ? otherResults : results;
}
Location _toLocation(
AnalysisNavigationParams mergedResults, NavigationTarget target) {
final targetFilePath = mergedResults.files[target.fileIndex];
final targetLineInfo = server.getLineInfo(targetFilePath);
return navigationTargetToLocation(targetFilePath, target, targetLineInfo);
}
LocationLink _toLocationLink(AnalysisNavigationParams mergedResults,
LineInfo sourceLineInfo, NavigationTarget target) {
final region = mergedResults.regions.first;
final targetFilePath = mergedResults.files[target.fileIndex];
final targetLineInfo = server.getLineInfo(targetFilePath);
return navigationTargetToLocationLink(
region, sourceLineInfo, targetFilePath, target, targetLineInfo);
}
}