| // 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 = []; | |
| /// A set of line numbers that already have folding regions defined. | |
| /// | |
| /// Multiple regions will not be produced that start on the same line because | |
| /// editors typically only show one folding action button per line. | |
| final _linesWithRegions = <int>{}; | |
| new(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/try 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. | |
| var 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) { | |
| _addRegion( | |
| firstDirective.keyword.end, | |
| lastDirective.directive.end, | |
| FoldingKind.DIRECTIVES, | |
| ); | |
| } | |
| _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. | |
| var 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)); | |
| } | |
| var 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; | |
| } | |
| } | |
| /// Adds a region from [startOffset] to [endOffset] for [kind]. | |
| /// | |
| /// If [startOffset] is the same line as a previous range and [fallbackStart] | |
| /// is provided (and not the same line), then [fallbackStart] will be used. | |
| void _addRegion( | |
| int startOffset, | |
| int endOffset, | |
| FoldingKind kind, { | |
| int? fallbackStart, | |
| }) { | |
| var start = _lineInfo.getLocation(startOffset); | |
| var end = _lineInfo.getLocation(endOffset); | |
| // We cannot start multiple regions on the same line, so if this region | |
| // starts on the same line as the previous region, try the fallback start. | |
| // | |
| // This usually happens with function declarations and parameters, where we | |
| // want to support folding long parameter lists but also be able to fold the | |
| // whole declaration (parameters + body). | |
| if (_linesWithRegions.contains(start.lineNumber)) { | |
| if (fallbackStart == null) { | |
| return; | |
| } | |
| startOffset = fallbackStart; | |
| start = _lineInfo.getLocation(startOffset); | |
| // If it's still the same, then we don't have a usable start. | |
| if (_linesWithRegions.contains(start.lineNumber)) { | |
| return; | |
| } | |
| } | |
| if (end.lineNumber > start.lineNumber) { | |
| _foldingRegions.add( | |
| FoldingRegion(kind, startOffset, endOffset - startOffset), | |
| ); | |
| _linesWithRegions.add(start.lineNumber); | |
| } | |
| } | |
| 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; | |
| new(this._computer); | |
| @override | |
| void visitArgumentList(ArgumentList node) { | |
| _computer._addRegion( | |
| node.leftParenthesis.end, | |
| node.rightParenthesis.offset, | |
| FoldingKind.INVOCATION, | |
| fallbackStart: node.arguments.firstOrNull?.offset, | |
| ); | |
| super.visitArgumentList(node); | |
| } | |
| @override | |
| void visitAssertInitializer(AssertInitializer node) { | |
| _computer._addRegion( | |
| node.leftParenthesis.end, | |
| node.rightParenthesis.offset, | |
| FoldingKind.INVOCATION, | |
| fallbackStart: node.condition.offset, | |
| ); | |
| super.visitAssertInitializer(node); | |
| } | |
| @override | |
| void visitAssertStatement(AssertStatement node) { | |
| _computer._addRegion( | |
| node.leftParenthesis.end, | |
| node.rightParenthesis.offset, | |
| FoldingKind.INVOCATION, | |
| ); | |
| super.visitAssertStatement(node); | |
| } | |
| @override | |
| void visitBlockClassBody(BlockClassBody node) { | |
| _computer._addRegion( | |
| node.leftBracket.end, | |
| node.rightBracket.offset, | |
| FoldingKind.CLASS_BODY, | |
| ); | |
| super.visitBlockClassBody(node); | |
| } | |
| @override | |
| void visitBlockEnumBody(BlockEnumBody node) { | |
| _computer._addRegion( | |
| node.leftBracket.end, | |
| node.rightBracket.offset, | |
| FoldingKind.CLASS_BODY, | |
| ); | |
| super.visitBlockEnumBody(node); | |
| } | |
| @override | |
| void visitClassDeclaration(ClassDeclaration node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| super.visitClassDeclaration(node); | |
| } | |
| @override | |
| void visitConstructorDeclaration(ConstructorDeclaration node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| // Find the most appropriate place to start. | |
| // For a named constructor, end of the name. | |
| // For unnamed constructor, end of the type name. | |
| // For `new`, end of the keyword. | |
| var bodyStart = | |
| node.name?.end ?? node.typeName?.end ?? node.newKeyword?.end; | |
| if (bodyStart != null) { | |
| _computer._addRegion(bodyStart, node.end, FoldingKind.FUNCTION_BODY); | |
| } | |
| 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); | |
| 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); | |
| super.visitExtensionDeclaration(node); | |
| } | |
| @override | |
| void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| super.visitExtensionTypeDeclaration(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, | |
| fallbackStart: node.parameters.firstOrNull?.offset, | |
| ); | |
| super.visitFormalParameterList(node); | |
| } | |
| @override | |
| void visitForStatement(ForStatement node) { | |
| var body = node.body; | |
| if (body is Block) { | |
| _computer._addRegion( | |
| body.leftBracket.offset, | |
| body.rightBracket.end, | |
| FoldingKind.BLOCK, | |
| ); | |
| } | |
| super.visitForStatement(node); | |
| } | |
| @override | |
| void visitFunctionDeclaration(FunctionDeclaration node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| _computer._addRegion(node.name.end, node.end, FoldingKind.FUNCTION_BODY); | |
| super.visitFunctionDeclaration(node); | |
| } | |
| @override | |
| void visitFunctionExpression(FunctionExpression node) { | |
| var body = node.body; | |
| if (body is BlockFunctionBody) { | |
| _computer._addRegion( | |
| body.block.leftBracket.offset, | |
| body.block.rightBracket.end, | |
| FoldingKind.BLOCK, | |
| ); | |
| } | |
| super.visitFunctionExpression(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 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); | |
| _computer._addRegion(node.name.end, node.end, FoldingKind.FUNCTION_BODY); | |
| super.visitMethodDeclaration(node); | |
| } | |
| @override | |
| void visitMixinDeclaration(MixinDeclaration node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| 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.ofKeyword)); | |
| super.visitPartOfDirective(node); | |
| } | |
| @override | |
| void visitPrimaryConstructorBody(PrimaryConstructorBody node) { | |
| _computer._addRegionForAnnotations(node.metadata); | |
| _computer._addRegion( | |
| node.thisKeyword.end, | |
| node.end, | |
| FoldingKind.FUNCTION_BODY, | |
| ); | |
| super.visitPrimaryConstructorBody(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 visitSimpleStringLiteral(SimpleStringLiteral node) { | |
| _computer._addRegion(node.offset, node.end, FoldingKind.LITERAL); | |
| super.visitSimpleStringLiteral(node); | |
| } | |
| @override | |
| void visitStringInterpolation(StringInterpolation node) { | |
| _computer._addRegion(node.offset, node.end, FoldingKind.LITERAL); | |
| super.visitStringInterpolation(node); | |
| } | |
| @override | |
| void visitSwitchCase(SwitchCase node) { | |
| _computer._addRegion(node.colon.end, node.end, FoldingKind.BLOCK); | |
| super.visitSwitchCase(node); | |
| } | |
| @override | |
| void visitSwitchDefault(SwitchDefault node) { | |
| _computer._addRegion(node.colon.end, node.end, FoldingKind.BLOCK); | |
| super.visitSwitchDefault(node); | |
| } | |
| @override | |
| void visitSwitchExpression(SwitchExpression node) { | |
| _computer._addRegion( | |
| node.leftBracket.end, | |
| node.rightBracket.end, | |
| FoldingKind.BLOCK, | |
| ); | |
| super.visitSwitchExpression(node); | |
| } | |
| @override | |
| void visitSwitchExpressionCase(SwitchExpressionCase node) { | |
| _computer._addRegion(node.arrow.end, node.end, FoldingKind.BLOCK); | |
| super.visitSwitchExpressionCase(node); | |
| } | |
| @override | |
| void visitSwitchPatternCase(SwitchPatternCase node) { | |
| _computer._addRegion(node.colon.end, node.end, FoldingKind.BLOCK); | |
| super.visitSwitchPatternCase(node); | |
| } | |
| @override | |
| void visitSwitchStatement(SwitchStatement node) { | |
| _computer._addRegion( | |
| node.leftBracket.end, | |
| node.rightBracket.end, | |
| FoldingKind.BLOCK, | |
| ); | |
| super.visitSwitchStatement(node); | |
| } | |
| @override | |
| void visitTryStatement(TryStatement node) { | |
| _computer.addRegionForConditionalBlock(node.body); | |
| for (var catchNode in node.catchClauses) { | |
| _computer.addRegionForConditionalBlock(catchNode.body); | |
| } | |
| if (node.finallyBlock case var finallyNode?) { | |
| _computer.addRegionForConditionalBlock(finallyNode); | |
| } | |
| super.visitTryStatement(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; | |
| new(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 { | |
| var 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('///'); | |
| } |