blob: 43820691df337365c12cdc60007094bb347c7b43 [file]
// 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>> {
new(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 until it
/// no longer ends on such a line (and if it's no longer multiple lines,
/// remove it).
void _compensateForLineFolding(List<FoldingRange> foldingRanges) {
// Create a set of all lines that start a folding range so we can ensure
// nothing ends on one of these lines.
var startLines = foldingRanges.map((r) => r.startLine).toSet();
for (var i = 0; i < foldingRanges.length; i++) {
var range = foldingRanges[i];
// No overlap, continue with the next.
if (!startLines.contains(range.endLine)) {
continue;
}
// Otherwise, wind back the end line to the first line that doesn't
// collide.
var newEndLine = range.endLine;
while (startLines.contains(newEndLine) &&
// Don't go any further if we hit the start line.
newEndLine >= range.startLine) {
newEndLine--;
}
// If the range is no longer multiple lines, remove it.
if (newEndLine <= range.startLine) {
foldingRanges.removeAt(i);
i--;
} else {
// Otherwise udpate the range with the new end line.
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> {
new(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;
}