7d91a99f55
At this point I'm not planning on adding folding for record types because we don't fold any other type annotations. Let me know if you think that's the wrong decision. Change-Id: Ibcac086fabfaf841ac3450cdd549accfb10c6188 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257440 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
403 lines
13 KiB
Dart
403 lines
13 KiB
Dart
// 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:analyzer/dart/ast/ast.dart';
|
|
import 'package:analyzer/dart/ast/token.dart';
|
|
import 'package:analyzer/dart/ast/visitor.dart';
|
|
import 'package:analyzer/source/line_info.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart';
|
|
|
|
/// A computer for [CompilationUnit] folding.
|
|
class DartUnitFoldingComputer {
|
|
final LineInfo _lineInfo;
|
|
final CompilationUnit _unit;
|
|
|
|
_Directive? _firstDirective, _lastDirective;
|
|
final List<FoldingRegion> _foldingRegions = [];
|
|
|
|
DartUnitFoldingComputer(this._lineInfo, this._unit);
|
|
|
|
void addRegionForConditionalBlock(Block block) {
|
|
// For class/function/method blocks, we usually include the whitespace up
|
|
// until the `}` in the folding region so that when collapsed they would
|
|
// look like:
|
|
//
|
|
// class Foo { [...] }
|
|
//
|
|
// For if statements, they may have else/elseIfs which would result in long
|
|
// lines like:
|
|
//
|
|
// if (cond) { [...] } else { [...] }
|
|
//
|
|
// So these types of blocks should have their folding regions end at the
|
|
// end of the preceding statement.
|
|
|
|
final start = block.leftBracket.end;
|
|
Token? comment = block.endToken.precedingComments;
|
|
if (comment != null) {
|
|
var lastComment = comment;
|
|
// If there are comments before the end token, use the last of those.
|
|
var nextComment = lastComment.next;
|
|
while (nextComment != null) {
|
|
lastComment = nextComment;
|
|
nextComment = nextComment.next;
|
|
}
|
|
_addRegion(start, lastComment.end, FoldingKind.BLOCK);
|
|
} else if (block.statements.isNotEmpty) {
|
|
// Otherwise, use the end of the last statement.
|
|
_addRegion(start, block.statements.last.end, FoldingKind.BLOCK);
|
|
}
|
|
}
|
|
|
|
/// Returns a list of folding regions, not `null`.
|
|
List<FoldingRegion> compute() {
|
|
_unit.accept(_DartUnitFoldingComputerVisitor(this));
|
|
|
|
var firstDirective = _firstDirective;
|
|
var lastDirective = _lastDirective;
|
|
if (firstDirective != null &&
|
|
lastDirective != null &&
|
|
firstDirective != lastDirective) {
|
|
_foldingRegions.add(
|
|
FoldingRegion(
|
|
FoldingKind.DIRECTIVES,
|
|
firstDirective.keyword.end,
|
|
lastDirective.directive.end - firstDirective.keyword.end,
|
|
),
|
|
);
|
|
}
|
|
|
|
_addCommentRegions();
|
|
|
|
return _foldingRegions;
|
|
}
|
|
|
|
/// Create a folding region for the provided comment, reading forwards if
|
|
/// necessary.
|
|
///
|
|
/// If [mayBeFileHeader] is true, the token will be considered a file header
|
|
/// if comment is a single-line-comment and there is a blank line or another
|
|
/// comment type after it.
|
|
///
|
|
/// Returns the next comment to be processed or null if there are no more
|
|
/// comments to process in the chain.
|
|
Token? _addCommentRegion(Token commentToken, {bool mayBeFileHeader = false}) {
|
|
int offset, end;
|
|
var isFileHeader = false;
|
|
Token? nextComment;
|
|
|
|
if (commentToken.type == TokenType.MULTI_LINE_COMMENT) {
|
|
// Multiline comments already span all of their lines but the folding
|
|
// region should start at the end of the first line.
|
|
offset = commentToken.offset + (commentToken.eolOffset ?? 0);
|
|
end = commentToken.end;
|
|
nextComment = commentToken.next;
|
|
} else {
|
|
// Single line comments need grouping together explicitly but should
|
|
// only group if the prefix is the same and up to any blank line.
|
|
final isTripleSlash = commentToken.isTripleSlash;
|
|
// Track the last comment that belongs to this folding region.
|
|
var lastComment = commentToken;
|
|
var current = lastComment.next;
|
|
while (current != null &&
|
|
current.type == lastComment.type &&
|
|
current.isTripleSlash == isTripleSlash &&
|
|
!_hasBlankLineBetween(lastComment.end, current.offset)) {
|
|
lastComment = current;
|
|
current = current.next;
|
|
}
|
|
|
|
// For single line comments we prefer to start the range at the end of
|
|
// first token so the first line is still visible when the range is
|
|
// collapsed.
|
|
offset = commentToken.end;
|
|
end = lastComment.end;
|
|
nextComment = lastComment.next;
|
|
|
|
// Single line comments are file headers if they're followed by a different
|
|
// comment type of there's a blank line between them and the first token.
|
|
isFileHeader = mayBeFileHeader &&
|
|
(nextComment != null ||
|
|
_hasBlankLineBetween(end, _unit.beginToken.offset));
|
|
}
|
|
|
|
final kind = isFileHeader
|
|
? FoldingKind.FILE_HEADER
|
|
: (commentToken.lexeme.startsWith('///') ||
|
|
commentToken.lexeme.startsWith('/**'))
|
|
? FoldingKind.DOCUMENTATION_COMMENT
|
|
: FoldingKind.COMMENT;
|
|
|
|
_addRegion(offset, end, kind);
|
|
|
|
return nextComment;
|
|
}
|
|
|
|
void _addCommentRegions() {
|
|
Token? token = _unit.beginToken;
|
|
if (token.type == TokenType.SCRIPT_TAG) {
|
|
token = token.next;
|
|
}
|
|
var isFirstToken = true;
|
|
while (token != null) {
|
|
Token? commentToken = token.precedingComments;
|
|
while (commentToken != null) {
|
|
commentToken =
|
|
_addCommentRegion(commentToken, mayBeFileHeader: isFirstToken);
|
|
}
|
|
isFirstToken = false;
|
|
// Only exit the loop when hitting EOF *after* processing the token as
|
|
// the EOF token may have preceding comments.
|
|
if (token.isEof) {
|
|
break;
|
|
}
|
|
token = token.next;
|
|
}
|
|
}
|
|
|
|
void _addRegion(int startOffset, int endOffset, FoldingKind kind) {
|
|
var start = _lineInfo.getLocation(startOffset);
|
|
var end = _lineInfo.getLocation(endOffset);
|
|
|
|
if (start.lineNumber != end.lineNumber) {
|
|
_foldingRegions
|
|
.add(FoldingRegion(kind, startOffset, endOffset - startOffset));
|
|
}
|
|
}
|
|
|
|
void _addRegionForAnnotations(List<Annotation> annotations) {
|
|
if (annotations.isNotEmpty) {
|
|
_addRegion(annotations.first.name.end, annotations.last.end,
|
|
FoldingKind.ANNOTATIONS);
|
|
}
|
|
}
|
|
|
|
bool _hasBlankLineBetween(int offset, int end) {
|
|
var firstLoc = _lineInfo.getLocation(offset);
|
|
var secondLoc = _lineInfo.getLocation(end);
|
|
return secondLoc.lineNumber - firstLoc.lineNumber > 1;
|
|
}
|
|
|
|
void _recordDirective(_Directive node) {
|
|
_firstDirective ??= node;
|
|
_lastDirective = node;
|
|
}
|
|
}
|
|
|
|
/// An AST visitor for [DartUnitFoldingComputer].
|
|
class _DartUnitFoldingComputerVisitor extends RecursiveAstVisitor<void> {
|
|
final DartUnitFoldingComputer _computer;
|
|
|
|
_DartUnitFoldingComputerVisitor(this._computer);
|
|
|
|
@override
|
|
void visitAssertInitializer(AssertInitializer node) {
|
|
_computer._addRegion(node.leftParenthesis.end, node.rightParenthesis.offset,
|
|
FoldingKind.INVOCATION);
|
|
super.visitAssertInitializer(node);
|
|
}
|
|
|
|
@override
|
|
void visitAssertStatement(AssertStatement node) {
|
|
_computer._addRegion(node.leftParenthesis.end, node.rightParenthesis.offset,
|
|
FoldingKind.INVOCATION);
|
|
super.visitAssertStatement(node);
|
|
}
|
|
|
|
@override
|
|
void visitBlockFunctionBody(BlockFunctionBody node) {
|
|
_computer._addRegion(node.block.leftBracket.end,
|
|
node.block.rightBracket.offset, FoldingKind.FUNCTION_BODY);
|
|
super.visitBlockFunctionBody(node);
|
|
}
|
|
|
|
@override
|
|
void visitClassDeclaration(ClassDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.CLASS_BODY);
|
|
super.visitClassDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitConstructorDeclaration(ConstructorDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
super.visitConstructorDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitDoStatement(DoStatement node) {
|
|
var body = node.body;
|
|
if (body is Block) {
|
|
_computer.addRegionForConditionalBlock(body);
|
|
}
|
|
super.visitDoStatement(node);
|
|
}
|
|
|
|
@override
|
|
void visitEnumDeclaration(EnumDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.CLASS_BODY);
|
|
super.visitEnumDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitExportDirective(ExportDirective node) {
|
|
_computer._recordDirective(_Directive(node, node.exportKeyword));
|
|
super.visitExportDirective(node);
|
|
}
|
|
|
|
@override
|
|
void visitExtensionDeclaration(ExtensionDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.CLASS_BODY);
|
|
super.visitExtensionDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitFieldDeclaration(FieldDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
super.visitFieldDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitFormalParameterList(FormalParameterList node) {
|
|
_computer._addRegion(node.leftParenthesis.end, node.rightParenthesis.offset,
|
|
FoldingKind.PARAMETERS);
|
|
super.visitFormalParameterList(node);
|
|
}
|
|
|
|
@override
|
|
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
super.visitFunctionDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
|
|
_computer._addRegion(node.argumentList.leftParenthesis.end,
|
|
node.argumentList.rightParenthesis.offset, FoldingKind.INVOCATION);
|
|
super.visitFunctionExpressionInvocation(node);
|
|
}
|
|
|
|
@override
|
|
void visitIfStatement(IfStatement node) {
|
|
var thenStatement = node.thenStatement;
|
|
if (thenStatement is Block) {
|
|
_computer.addRegionForConditionalBlock(thenStatement);
|
|
}
|
|
var elseStatement = node.elseStatement;
|
|
if (elseStatement is Block) {
|
|
_computer.addRegionForConditionalBlock(elseStatement);
|
|
}
|
|
super.visitIfStatement(node);
|
|
}
|
|
|
|
@override
|
|
void visitImportDirective(ImportDirective node) {
|
|
_computer._recordDirective(_Directive(node, node.importKeyword));
|
|
super.visitImportDirective(node);
|
|
}
|
|
|
|
@override
|
|
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
_computer._addRegion(node.argumentList.leftParenthesis.end,
|
|
node.argumentList.rightParenthesis.offset, FoldingKind.INVOCATION);
|
|
super.visitInstanceCreationExpression(node);
|
|
}
|
|
|
|
@override
|
|
void visitLibraryDirective(LibraryDirective node) {
|
|
_computer._recordDirective(_Directive(node, node.libraryKeyword));
|
|
super.visitLibraryDirective(node);
|
|
}
|
|
|
|
@override
|
|
void visitListLiteral(ListLiteral node) {
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.LITERAL);
|
|
super.visitListLiteral(node);
|
|
}
|
|
|
|
@override
|
|
void visitMethodDeclaration(MethodDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
super.visitMethodDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitMethodInvocation(MethodInvocation node) {
|
|
_computer._addRegion(node.argumentList.leftParenthesis.end,
|
|
node.argumentList.rightParenthesis.offset, FoldingKind.INVOCATION);
|
|
super.visitMethodInvocation(node);
|
|
}
|
|
|
|
@override
|
|
void visitMixinDeclaration(MixinDeclaration node) {
|
|
_computer._addRegionForAnnotations(node.metadata);
|
|
// TODO(brianwilkerson) Define `FoldingKind.MIXIN_BODY`?
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.CLASS_BODY);
|
|
super.visitMixinDeclaration(node);
|
|
}
|
|
|
|
@override
|
|
void visitPartDirective(PartDirective node) {
|
|
_computer._recordDirective(_Directive(node, node.partKeyword));
|
|
super.visitPartDirective(node);
|
|
}
|
|
|
|
@override
|
|
void visitPartOfDirective(PartOfDirective node) {
|
|
_computer._recordDirective(_Directive(node, node.partKeyword));
|
|
super.visitPartOfDirective(node);
|
|
}
|
|
|
|
@override
|
|
void visitRecordLiteral(RecordLiteral node) {
|
|
_computer._addRegion(node.leftParenthesis.end, node.rightParenthesis.offset,
|
|
FoldingKind.LITERAL);
|
|
super.visitRecordLiteral(node);
|
|
}
|
|
|
|
@override
|
|
void visitSetOrMapLiteral(SetOrMapLiteral node) {
|
|
_computer._addRegion(
|
|
node.leftBracket.end, node.rightBracket.offset, FoldingKind.LITERAL);
|
|
super.visitSetOrMapLiteral(node);
|
|
}
|
|
|
|
@override
|
|
void visitWhileStatement(WhileStatement node) {
|
|
var body = node.body;
|
|
if (body is Block) {
|
|
_computer.addRegionForConditionalBlock(body);
|
|
}
|
|
super.visitWhileStatement(node);
|
|
}
|
|
}
|
|
|
|
class _Directive {
|
|
final Directive directive;
|
|
final Token keyword;
|
|
|
|
_Directive(this.directive, this.keyword);
|
|
}
|
|
|
|
extension _CommentTokenExtensions on Token {
|
|
static final _newlinePattern = RegExp(r'[\r\n]');
|
|
|
|
/// Return the offset of the first eol character or `null` if no newlines were
|
|
/// found.
|
|
int? get eolOffset {
|
|
final offset = lexeme.indexOf(_newlinePattern);
|
|
return offset != -1 ? offset : null;
|
|
}
|
|
|
|
/// Return `true` if this comment is a triple-slash single line comment.
|
|
bool get isTripleSlash => lexeme.startsWith('///');
|
|
}
|