Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b94adb2

Browse files
DanTupcommit-bot@chromium.org
authored andcommitted
[Analyzer] Add folding regions for comment blocks
Change-Id: Iafe70e977065dc68230503349b8ba3abe9b4c0c0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/179777 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
1 parent 134a437 commit b94adb2

File tree

15 files changed

+222
-62
lines changed

15 files changed

+222
-62
lines changed

pkg/analysis_server/doc/api.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
<body>
110110
<h1>Analysis Server API Specification</h1>
111111
<h1 style="color:#999999">Version
112-
1.32.1
112+
1.32.2
113113
</h1>
114114
<p>
115115
This document contains a specification of the API provided by the
@@ -236,9 +236,13 @@ <h3>Enumerations</h3>
236236
ignoring the item or treating it with some default/fallback handling.
237237
</p>
238238
<h3>Changelog</h3>
239+
<h4>1.32.2</h4>
240+
<ul>
241+
<li>Added <tt>FoldingKind.COMMENT</tt> for folding regions for blocks of comments.</li>
242+
</ul>
239243
<h4>1.32.1</h4>
240244
<ul>
241-
<li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt></li>
245+
<li>Added <tt>CompletionSuggestionKind.PACKAGE_NAME</tt> for Pub package name completions in <tt>pubspec.yaml</tt>.</li>
242246
</ul>
243247
<h3>Domains</h3>
244248
<p>
@@ -4226,7 +4230,7 @@ <h2 class="domain"><a name="types">Types</a></h2>
42264230
An enumeration of the kinds of folding regions.
42274231
</p>
42284232

4229-
<dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
4233+
<dl><dt class="value">ANNOTATIONS</dt><dt class="value">BLOCK</dt><dt class="value">CLASS_BODY</dt><dt class="value">COMMENT</dt><dt class="value">DIRECTIVES</dt><dt class="value">DOCUMENTATION_COMMENT</dt><dt class="value">FILE_HEADER</dt><dt class="value">FUNCTION_BODY</dt><dt class="value">INVOCATION</dt><dt class="value">LITERAL</dt></dl></dd><dt class="typeDefinition"><a name="type_FoldingRegion">FoldingRegion: object</a></dt><dd>
42304234
<p>
42314235
A description of a region that can be folded.
42324236
</p>

pkg/analysis_server/lib/protocol/protocol_constants.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// To regenerate the file, use the script
77
// "pkg/analysis_server/tool/spec/generate_files".
88

9-
const String PROTOCOL_VERSION = '1.32.1';
9+
const String PROTOCOL_VERSION = '1.32.2';
1010

1111
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES = 'analysis.analyzedFiles';
1212
const String ANALYSIS_NOTIFICATION_ANALYZED_FILES_DIRECTORIES = 'directories';

pkg/analysis_server/lib/src/computer/computer_folding.dart

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

pkg/analysis_server/lib/src/lsp/mapping.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ lsp.FoldingRange toFoldingRange(
10551055

10561056
lsp.FoldingRangeKind toFoldingRangeKind(server.FoldingKind kind) {
10571057
switch (kind) {
1058+
case server.FoldingKind.COMMENT:
10581059
case server.FoldingKind.DOCUMENTATION_COMMENT:
10591060
case server.FoldingKind.FILE_HEADER:
10601061
return lsp.FoldingRangeKind.Comment;

pkg/analysis_server/test/integration/support/protocol_matchers.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ final Matcher isFlutterWidgetPropertyValueEnumItem = LazyMatcher(() =>
723723
/// ANNOTATIONS
724724
/// BLOCK
725725
/// CLASS_BODY
726+
/// COMMENT
726727
/// DIRECTIVES
727728
/// DOCUMENTATION_COMMENT
728729
/// FILE_HEADER
@@ -734,6 +735,7 @@ final Matcher isFoldingKind = MatchesEnum('FoldingKind', [
734735
'ANNOTATIONS',
735736
'BLOCK',
736737
'CLASS_BODY',
738+
'COMMENT',
737739
'DIRECTIVES',
738740
'DOCUMENTATION_COMMENT',
739741
'FILE_HEADER',

pkg/analysis_server/test/lsp/folding_test.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class FoldingTest extends AbstractLspAnalysisServerTest {
4444

4545
Future<void> test_comments() async {
4646
final content = '''
47-
[[/// This is a comment
47+
/// This is a comment[[
4848
/// that spans many lines]]
4949
class MyClass2 {}
5050
''';
@@ -164,21 +164,14 @@ class FoldingTest extends AbstractLspAnalysisServerTest {
164164
}
165165

166166
Future<void> test_headersImportsComments() async {
167-
// TODO(dantup): Review why the file header and the method comment ranges
168-
// are different... one spans only the range to collapse, but the other
169-
// just starts at the logical block.
170-
// The LSP spec doesn't give any guidance on whether the first part of
171-
// the surrounded content should be visible or not after folding
172-
// so we'll need to revisit this once there's clarification:
173-
// https://github.com/Microsoft/language-server-protocol/issues/659
174167
final content = '''
175168
// Copyright some year by some people[[
176169
// See LICENCE etc.]]
177170
178171
import[[ 'dart:io';
179172
import 'dart:async';]]
180173
181-
[[/// This is not the file header
174+
/// This is not the file header[[
182175
/// It's just a comment]]
183176
main() {}
184177
''';

0 commit comments

Comments
 (0)