blob: a4813e6174e72939161fe6e9b117961370b667a4 [file] [log] [blame]
// Copyright (c) 2019, 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';
import 'package:analysis_server/src/computer/computer_folding.dart';
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/protocol_server.dart';
import 'package:analyzer/source/line_info.dart';
typedef StaticOptions =
Either3<bool, FoldingRangeOptions, FoldingRangeRegistrationOptions>;
class FoldingHandler
extends LspMessageHandler<FoldingRangeParams, List<FoldingRange>> {
FoldingHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_foldingRange;
@override
LspJsonHandler<FoldingRangeParams> get jsonHandler =>
FoldingRangeParams.jsonHandler;
@override
Future<ErrorOr<List<FoldingRange>>> handle(
FoldingRangeParams params,
MessageInfo message,
CancellationToken token,
) async {
var clientCapabilities = message.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return serverNotInitializedError;
}
var lineFoldingOnly = clientCapabilities.lineFoldingOnly;
var path = pathOfDoc(params.textDocument);
return path.mapResult((path) async {
var partialResults = <List<FoldingRegion>>[];
LineInfo? lineInfo;
var unit = await server.getParsedUnit(path);
if (unit != null) {
lineInfo = unit.lineInfo;
var regions = DartUnitFoldingComputer(lineInfo, unit.unit).compute();
partialResults.insert(0, regions);
}
// Still try to obtain line info for invalid or non-Dart files, as plugins
// could contribute to those.
lineInfo ??= server.getLineInfo(path);
if (lineInfo == null) {
// Line information would be required to translate folding results to
// LSP.
return success(const []);
}
var notificationManager = server.notificationManager;
var pluginResults = notificationManager.folding.getResults(path);
partialResults.addAll(pluginResults);
var regions = notificationManager.merger.mergeFoldingRegions(
partialResults,
);
// Ensure sorted by offset for when looking for overlapping ranges in
// line mode below.
regions.sort((r1, r2) => r1.offset.compareTo(r2.offset));
var foldingRanges =
regions
.map(
(region) => _toFoldingRange(
lineInfo!,
region,
lineOnly: lineFoldingOnly,
),
)
.toList();
// When in line-only mode, ranges that end on the same line that another
// ranges starts should be truncated to be on the line before (and if this
// leave them spanning only a single line, should be removed).
if (lineFoldingOnly) {
_compensateForLineFolding(foldingRanges);
}
return success(foldingRanges);
});
}
/// Adjust [foldingRanges] taking into count additional rules for line
/// folding.
///
/// When character folding is supported, a range may start on the same line
/// that another ends (as long as they don't overlap).
///
/// When only line folding is supported, ranges must not end on the same line
/// that another starts. In this case, we shrink the previous range (and if
/// this makes it a single line, remove it).
void _compensateForLineFolding(List<FoldingRange> foldingRanges) {
// Loop over items except last (`-1`). We can skip the last item because
// it has no next item.
for (var i = 0; i < foldingRanges.length - 1; i++) {
var range = foldingRanges[i];
var next = foldingRanges[i + 1];
// If this item runs into the next but does not completely enclose it...
if (range.endLine >= next.startLine && range.endLine <= next.endLine) {
// Truncate it to end on the line before.
var newEndLine = next.startLine - 1;
// If it no longer needs to be a folding range at all, remove it.
if (newEndLine <= range.startLine) {
foldingRanges.removeAt(i);
i--;
continue;
}
foldingRanges[i] = FoldingRange(
startLine: range.startLine,
endLine: newEndLine,
kind: range.kind,
);
}
}
}
FoldingRange _toFoldingRange(
LineInfo lineInfo,
FoldingRegion region, {
required bool lineOnly,
}) {
var range = toRange(lineInfo, region.offset, region.length);
return FoldingRange(
startLine: range.start.line,
startCharacter: lineOnly ? null : range.start.character,
endLine: range.end.line,
endCharacter: lineOnly ? null : range.end.character,
kind: toFoldingRangeKind(region.kind),
);
}
}
class FoldingRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
FoldingRegistrations(super.info);
@override
ToJsonable? get options =>
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes);
@override
Method get registrationMethod => Method.textDocument_foldingRange;
@override
StaticOptions get staticOptions => Either3.t1(true);
@override
bool get supportsDynamic => clientDynamic.folding;
}