@@ -49,7 +49,6 @@ class DartUnitFoldingComputer {
4949
5050 /// Returns a list of folding regions, not `null` .
5151 List <FoldingRegion > compute () {
52- _addFileHeaderRegion ();
5352 _unit.accept (_DartUnitFoldingComputerVisitor (this ));
5453
5554 if (_firstDirective != null &&
@@ -61,39 +60,90 @@ class DartUnitFoldingComputer {
6160 _lastDirective.end - _firstDirective.keyword.end));
6261 }
6362
63+ _addCommentRegions ();
64+
6465 return _foldingRegions;
6566 }
6667
67- void _addFileHeaderRegion () {
68- var firstToken = _unit.beginToken;
69- while (firstToken? .type == TokenType .SCRIPT_TAG ) {
70- firstToken = firstToken.next;
71- }
68+ /// Create a folding region for the provided comment, reading forwards if neccesary.
69+ ///
70+ /// If [mayBeFileHeader] is true, the token will be considered a file header
71+ /// if comment is a single-line-comment and there is a blank line or another
72+ /// comment type after it.
73+ ///
74+ /// Returns the next comment to be processed or null if there are no more comments
75+ /// to process in the chain.
76+ Token _addCommentRegion (Token commentToken, {bool mayBeFileHeader = false }) {
77+ int offset, end;
78+ var isFileHeader = false ;
79+ Token nextComment;
80+
81+ if (commentToken.type == TokenType .MULTI_LINE_COMMENT ) {
82+ // Multiline comments already span all of their lines but the folding
83+ // region should start at the end of the first line.
84+ offset = commentToken.offset + (commentToken.eolOffset ?? 0 );
85+ end = commentToken.end;
86+ nextComment = commentToken.next;
87+ } else {
88+ // Single line comments need grouping together explicitly but should
89+ // only group if the prefix is the same and up to any blank line.
90+ final isTripleSlash = commentToken.isTripleSlash;
91+ // Track the last comment that belongs to this folding region.
92+ var lastComment = commentToken;
93+ var current = lastComment.next;
94+ while (current != null &&
95+ current.type == lastComment.type &&
96+ current.isTripleSlash == isTripleSlash &&
97+ ! _hasBlankLineBetween (lastComment.end, current.offset)) {
98+ lastComment = current;
99+ current = current.next;
100+ }
72101
73- final Token firstComment = firstToken? .precedingComments;
74- if (firstComment == null ||
75- firstComment.type != TokenType .SINGLE_LINE_COMMENT ) {
76- return ;
102+ // For single line comments we prefer to start the range at the end of
103+ // first token so the first line is still visible when the range is
104+ // collapsed.
105+ offset = commentToken.end;
106+ end = lastComment.end;
107+ nextComment = lastComment.next;
108+
109+ // Single line comments are file headers if they're followed by a different
110+ // comment type of there's a blank line between them and the first token.
111+ isFileHeader = mayBeFileHeader &&
112+ (nextComment != null ||
113+ _hasBlankLineBetween (end, _unit.beginToken.offset));
77114 }
78115
79- // Walk through the comments looking for a blank line to signal the end of
80- // the file header.
81- var lastComment = firstComment;
82- while (lastComment.next != null ) {
83- lastComment = lastComment.next;
116+ final kind = isFileHeader
117+ ? FoldingKind .FILE_HEADER
118+ : (commentToken.lexeme.startsWith ('///' ) ||
119+ commentToken.lexeme.startsWith ('/**' ))
120+ ? FoldingKind .DOCUMENTATION_COMMENT
121+ : FoldingKind .COMMENT ;
84122
85- // If we ran out of tokens, use the original token as starting position.
86- final hasBlankLine =
87- _hasBlankLineBetween (lastComment, lastComment.next ?? firstToken);
123+ _addRegion (offset, end, kind);
88124
89- // Also considered non-single-line-comments as the end
90- final nextCommentIsDifferentType = lastComment.next != null &&
91- lastComment.next.type != TokenType .SINGLE_LINE_COMMENT ;
125+ return nextComment;
126+ }
92127
93- if (hasBlankLine || nextCommentIsDifferentType) {
94- _addRegion (firstComment.end, lastComment.end, FoldingKind .FILE_HEADER );
128+ void _addCommentRegions () {
129+ var token = _unit.beginToken;
130+ if (token.type == TokenType .SCRIPT_TAG ) {
131+ token = token.next;
132+ }
133+ var isFirstToken = true ;
134+ while (token != null ) {
135+ Token commentToken = token.precedingComments;
136+ while (commentToken != null ) {
137+ commentToken =
138+ _addCommentRegion (commentToken, mayBeFileHeader: isFirstToken);
139+ }
140+ isFirstToken = false ;
141+ // Only exit the loop when hitting EOF *after* processing the token as
142+ // the EOF token may have preceeding comments.
143+ if (token.type == TokenType .EOF ) {
95144 break ;
96145 }
146+ token = token.next;
97147 }
98148 }
99149
@@ -114,9 +164,9 @@ class DartUnitFoldingComputer {
114164 }
115165 }
116166
117- bool _hasBlankLineBetween (Token first, Token second ) {
118- final CharacterLocation firstLoc = _lineInfo.getLocation (first.end );
119- final CharacterLocation secondLoc = _lineInfo.getLocation (second.offset );
167+ bool _hasBlankLineBetween (int offset, int end ) {
168+ final CharacterLocation firstLoc = _lineInfo.getLocation (offset );
169+ final CharacterLocation secondLoc = _lineInfo.getLocation (end );
120170 return secondLoc.lineNumber - firstLoc.lineNumber > 1 ;
121171 }
122172
@@ -161,15 +211,6 @@ class _DartUnitFoldingComputerVisitor extends RecursiveAstVisitor<void> {
161211 super .visitClassDeclaration (node);
162212 }
163213
164- @override
165- void visitComment (Comment node) {
166- if (node.isDocumentation) {
167- _computer._addRegion (
168- node.offset, node.end, FoldingKind .DOCUMENTATION_COMMENT );
169- }
170- super .visitComment (node);
171- }
172-
173214 @override
174215 void visitConstructorDeclaration (ConstructorDeclaration node) {
175216 _computer._addRegionForAnnotations (node.metadata);
@@ -303,3 +344,17 @@ class _DartUnitFoldingComputerVisitor extends RecursiveAstVisitor<void> {
303344 super .visitWhileStatement (node);
304345 }
305346}
347+
348+ extension _CommentTokenExtensions on Token {
349+ static final _newlinePattern = RegExp (r'[\r\n]' );
350+
351+ /// The offset of the first eol character or null
352+ /// if no newlines were found.
353+ int get eolOffset {
354+ final offset = lexeme.indexOf (_newlinePattern);
355+ return offset != - 1 ? offset : null ;
356+ }
357+
358+ /// Whether this comment is a triple-slash single line comment.
359+ bool get isTripleSlash => lexeme.startsWith ('///' );
360+ }
0 commit comments