| // 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; |
| } |