Repository: akvelon/flutter-code-editor
Pull request #65 (2022-09-13 12:34:06)

issue 64 hidden comments dont generate foldable blocks


Patch:
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index 709356e..a1ea140 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -11,6 +11,7 @@ import '../hidden_ranges/hidden_ranges.dart';
 import '../issues/issue.dart';
 import '../named_sections/named_section.dart';
 import '../named_sections/parsers/abstract.dart';
+import '../service_comment_filter/service_comment_filter.dart';
 import '../single_line_comments/parser/single_line_comment_parser.dart';
 import '../single_line_comments/parser/single_line_comments.dart';
 import '../single_line_comments/single_line_comment.dart';
@@ -46,17 +47,12 @@ class Code {
       singleLineCommentSequences: sequences,
     );
 
-    final serviceComments = _filterServiceComments(
+    final serviceComments = ServiceCommentFilter.filter(
       commentParser.comments,
       namedSectionParser: namedSectionParser,
     );
 
-    final lines = _textToCodeLines(
-      text: text,
-      highlighted: highlighted,
-      language: language,
-      commentsByLines: commentParser.getCommentsByLines(),
-    );
+    final serviceCommentsNodesSet = serviceComments.sources;
 
     final issues = <Issue>[];
     final List<FoldableBlock> foldableBlocks;
@@ -65,7 +61,7 @@ class Code {
       foldableBlocks = const [];
     } else {
       final parser = HighlightFoldableBlockParser();
-      parser.parse(highlighted);
+      parser.parse(highlighted, serviceCommentsNodesSet);
       foldableBlocks = parser.blocks;
       issues.addAll(parser.invalidBlocks.map((b) => b.issue));
     }
@@ -76,6 +72,13 @@ class Code {
         const [];
     final sectionsMap = {for (final s in sections) s.name: s};
 
+    final lines = _textToCodeLines(
+      text: text,
+      highlighted: highlighted,
+      language: language,
+      commentsByLines: commentParser.getCommentsByLines(),
+    );
+
     _applyNamedSectionsToLines(
       lines: lines,
       sections: sectionsMap,
@@ -125,38 +128,6 @@ class Code {
     visibleText: '',
   );
 
-  static Iterable<SingleLineComment> _filterServiceComments(
-    List<SingleLineComment> comments, {
-    required AbstractNamedSectionParser? namedSectionParser,
-  }) sync* {
-    for (final comment in comments) {
-      if (_isServiceComment(comment, namedSectionParser: namedSectionParser)) {
-        yield comment;
-      }
-    }
-  }
-
-  static bool _isServiceComment(
-    SingleLineComment comment, {
-    required AbstractNamedSectionParser? namedSectionParser,
-  }) {
-    final words = _getCommentWords(comment.innerContent);
-    if (words.contains(Tokens.readonly)) {
-      return true;
-    }
-
-    if (namedSectionParser != null) {
-      final namedSections = namedSectionParser.parseUnsorted(
-        singleLineComments: [comment],
-      );
-      if (namedSections.isNotEmpty) {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
   static List<CodeLine> _textToCodeLines({
     required String text,
     required Result? highlighted,
diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart
index 9c47bb3..c075ccb 100644
--- a/lib/src/code_field/code_field.dart
+++ b/lib/src/code_field/code_field.dart
@@ -7,7 +7,6 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 import '../code_theme/code_theme.dart';
 import '../gutter/gutter.dart';
 import '../line_numbers/line_number_style.dart';
-import '../sizes.dart';
 import '../wip/autocomplete/popup.dart';
 import 'code_controller.dart';
 
@@ -90,8 +89,8 @@ class CodeFieldState extends State<CodeField> {
   ScrollController? _horizontalCodeScroll;
   final _codeFieldKey = GlobalKey();
 
-  Offset _normalPopupOffset = Offset.zero;
-  Offset _flippedPopupOffset = Offset.zero;
+  double cursorX = 0;
+  double cursorY = 0;
   double painterWidth = 0;
   double painterHeight = 0;
 
@@ -111,15 +110,15 @@ class CodeFieldState extends State<CodeField> {
 
     widget.controller.addListener(_onTextChanged);
     widget.controller.addListener(() {
-      _updatePopupOffset(widget.controller.text);
+      _updateCursorOffset(widget.controller.text);
     });
     _horizontalCodeScroll = ScrollController();
     _focusNode = widget.focusNode ?? FocusNode();
     _focusNode!.attach(context, onKeyEvent: _onKeyEvent);
 
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      final double width = _codeFieldKey.currentContext!.size!.width;
-      final double height = _codeFieldKey.currentContext!.size!.height;
+      double width = _codeFieldKey.currentContext!.size!.width;
+      double height = _codeFieldKey.currentContext!.size!.height;
       windowSize = Size(width, height);
     });
     _onTextChanged();
@@ -133,7 +132,7 @@ class CodeFieldState extends State<CodeField> {
   void dispose() {
     widget.controller.removeListener(_onTextChanged);
     widget.controller.removeListener(() {
-      _updatePopupOffset(widget.controller.text);
+      _updateCursorOffset(widget.controller.text);
     });
     _numberScroll?.dispose();
     _codeScroll?.dispose();
@@ -149,8 +148,8 @@ class CodeFieldState extends State<CodeField> {
         // so check first.
         final context = _codeFieldKey.currentContext;
         if (context != null) {
-          final double width = context.size!.width;
-          final double height = context.size!.height;
+          double width = context.size!.width;
+          double height = context.size!.height;
           windowSize = Size(width, height);
         }
       });
@@ -307,16 +306,17 @@ class CodeFieldState extends State<CodeField> {
             child: Stack(
               children: [
                 editingField,
-                if (widget.controller.popupController.isPopupShown)
-                  Popup(
-                    normalOffset: _normalPopupOffset,
-                    flippedOffset: _flippedPopupOffset,
-                    controller: widget.controller.popupController,
-                    editingWindowSize: windowSize,
-                    style: textStyle,
-                    backgroundColor: backgroundCol,
-                    parentFocusNode: _focusNode!,
-                  ),
+                widget.controller.popupController.isPopupShown
+                    ? Popup(
+                        row: cursorY,
+                        column: cursorX,
+                        controller: widget.controller.popupController,
+                        editingWindowSize: windowSize,
+                        style: textStyle,
+                        backgroundColor: backgroundCol,
+                        parentFocusNode: _focusNode!,
+                      )
+                    : Container(),
               ],
             ),
           ),
@@ -325,60 +325,32 @@ class CodeFieldState extends State<CodeField> {
     );
   }
 
-  void _updatePopupOffset(String text) {
-    final TextPainter textPainter = _getTextPainter(text);
-    final caretHeight = _getCaretHeight(textPainter);
-
-    final double leftOffset = _getPopupLeftOffset(textPainter);
-    final double normalTopOffset = _getPopupTopOffset(textPainter, caretHeight);
-    final double flippedTopOffset = normalTopOffset -
-        (Sizes.autocompletePopupMaxHeight + caretHeight + Sizes.caretPadding);
-
-    setState(() {
-      _normalPopupOffset = Offset(leftOffset, normalTopOffset);
-      _flippedPopupOffset = Offset(leftOffset, flippedTopOffset);
-    });
-  }
-
-  TextPainter _getTextPainter(String text) {
-    return TextPainter(
+  void _updateCursorOffset(String text) {
+    TextPainter painter = TextPainter(
       textDirection: TextDirection.ltr,
       text: TextSpan(text: text, style: textStyle),
     )..layout();
-  }
-
-  Offset _getCaretOffset(TextPainter textPainter) {
-    return textPainter.getOffsetForCaret(
-      widget.controller.selection.base,
-      Rect.zero,
-    );
-  }
+    TextPosition cursorTextPosition = widget.controller.selection.base;
+    Rect caretPrototype = Rect.zero;
+    Offset caretOffset =
+        painter.getOffsetForCaret(cursorTextPosition, caretPrototype);
+    double caretHeight = (widget.controller.selection.base.offset > 0)
+        ? painter.getFullHeightForCaret(cursorTextPosition, caretPrototype)!
+        : 0;
 
-  double _getCaretHeight(TextPainter textPainter) {
-    final double? caretFullHeight = textPainter.getFullHeightForCaret(
-      widget.controller.selection.base,
-      Rect.zero,
-    );
-    return (widget.controller.selection.base.offset > 0) ? caretFullHeight! : 0;
-  }
-
-  double _getPopupLeftOffset(TextPainter textPainter) {
-    return max(
-      _getCaretOffset(textPainter).dx +
-          widget.padding.left -
-          _horizontalCodeScroll!.offset,
-      0,
-    );
-  }
-
-  double _getPopupTopOffset(TextPainter textPainter, double caretHeight) {
-    return max(
-      _getCaretOffset(textPainter).dy +
-          caretHeight +
-          16 +
-          widget.padding.top -
-          _codeScroll!.offset,
-      0,
-    );
+    setState(() {
+      cursorX = max(
+          caretOffset.dx + widget.padding.left - _horizontalCodeScroll!.offset,
+          0);
+      cursorY = max(
+          caretOffset.dy +
+              caretHeight +
+              16 +
+              widget.padding.top -
+              _codeScroll!.offset,
+          0);
+      painterWidth = painter.width;
+      painterHeight = painter.height;
+    });
   }
 }
diff --git a/lib/src/folding/parsers/highlight.dart b/lib/src/folding/parsers/highlight.dart
index 89e705d..7f178e4 100644
--- a/lib/src/folding/parsers/highlight.dart
+++ b/lib/src/folding/parsers/highlight.dart
@@ -31,23 +31,25 @@ class HighlightFoldableBlockParser extends AbstractFoldableBlockParser {
   /// not a comment.
   bool _foundNonWhitespace = false;
 
-  void parse(Result highlighted) {
+  void parse(Result highlighted, Set<Object?> serviceCommentsSources) {
     if (highlighted.nodes != null) {
-      _processNodes(highlighted.nodes!);
+      _processNodes(highlighted.nodes!, serviceCommentsSources);
     }
 
     _submitLine(); // In case the last one did not end with '\n'.
     finalize();
   }
 
-  void _processNodes(List<Node> nodes) {
-    nodes.forEach(_processNode);
+  void _processNodes(List<Node> nodes, Set<Object?> serviceCommentsSources) {
+    for (final node in nodes) {
+      _processNode(node, serviceCommentsSources);
+    }
   }
 
-  void _processNode(Node node) {
+  void _processNode(Node node, Set<Object?> serviceCommentsSources) {
     switch (node.className) {
       case NodeClasses.comment:
-        _processComment(node);
+        _processComment(node, serviceCommentsSources);
         break;
 
       case NodeClasses.keyword:
@@ -59,13 +61,21 @@ class HighlightFoldableBlockParser extends AbstractFoldableBlockParser {
         break;
 
       default:
-        _processDefault(node);
+        _processDefault(node, serviceCommentsSources);
     }
   }
 
-  void _processComment(Node node) {
+  void _processComment(Node node, Set<Object?> serviceComments) {
     final newlineCount = node.getNewlineCount();
 
+    if (_foundNonWhitespace) {
+      return;
+    }
+
+    if (serviceComments.contains(node)) {
+      return;
+    }
+
     if (newlineCount == 0) {
       _foundSingleLineComment = true;
       return;
@@ -114,11 +124,11 @@ class HighlightFoldableBlockParser extends AbstractFoldableBlockParser {
     _lineIndex += newlineCount;
   }
 
-  void _processDefault(Node node) {
+  void _processDefault(Node node, Set<Object?> serviceCommentsSources) {
     _processDefaultValue(node);
 
     if (node.children != null) {
-      _processNodes(node.children!);
+      _processNodes(node.children!, serviceCommentsSources);
     }
   }
 
diff --git a/lib/src/service_comment_filter/service_comment_filter.dart b/lib/src/service_comment_filter/service_comment_filter.dart
new file mode 100644
index 0000000..f29d064
--- /dev/null
+++ b/lib/src/service_comment_filter/service_comment_filter.dart
@@ -0,0 +1,43 @@
+import '../../flutter_code_editor.dart';
+import '../single_line_comments/single_line_comment.dart';
+
+class ServiceCommentFilter {
+  static Iterable<SingleLineComment> filter(
+    List<SingleLineComment> comments, {
+    required AbstractNamedSectionParser? namedSectionParser,
+  }) sync* {
+    for (final comment in comments) {
+      if (_isServiceComment(comment, namedSectionParser: namedSectionParser)) {
+        yield comment;
+      }
+    }
+  }
+
+  static bool _isServiceComment(
+    SingleLineComment comment, {
+    required AbstractNamedSectionParser? namedSectionParser,
+  }) {
+    final words = _getCommentWords(comment.innerContent);
+    if (words.contains(Tokens.readonly)) {
+      return true;
+    }
+
+    if (namedSectionParser != null) {
+      final namedSections = namedSectionParser.parseUnsorted(
+        singleLineComments: [comment],
+      );
+      if (namedSections.isNotEmpty) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  static List<String> _getCommentWords(
+    String? comment,
+  ) {
+    // Split by any whitespaces.
+    return comment?.split(RegExp(r'\s+')) ?? const <String>[];
+  }
+}
diff --git a/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart b/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
index 794c25b..5c5969a 100644
--- a/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
+++ b/lib/src/single_line_comments/parser/highlight_single_line_comment_parser.dart
@@ -35,6 +35,7 @@ class HighlightSingleLineCommentParser extends AbstractSingleLineCommentParser {
               lineIndex: lineIndex,
               characterIndex: characterIndex,
               sequences: singleLineCommentSequences,
+              source: node,
             ),
           );
         }
diff --git a/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart b/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
index 7ed3859..3d9ad6b 100644
--- a/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
+++ b/lib/src/single_line_comments/parser/text_single_line_comment_parser.dart
@@ -32,6 +32,7 @@ class TextSingleLineCommentParser extends AbstractSingleLineCommentParser {
             lineIndex: lineIndex,
             characterIndex: characterIndex + column,
             sequences: singleLineCommentSequences,
+            source: line,
           ),
         );
       }
diff --git a/lib/src/single_line_comments/single_line_comment.dart b/lib/src/single_line_comments/single_line_comment.dart
index e079187..1cc7458 100644
--- a/lib/src/single_line_comments/single_line_comment.dart
+++ b/lib/src/single_line_comments/single_line_comment.dart
@@ -14,11 +14,17 @@ class SingleLineComment {
   /// The comment text with the characters signifying the comment.
   final String outerContent;
 
+  /// The object this comment was parsed from, if any.
+  ///
+  /// The object class is specific to the parser.
+  final Object? source;
+
   const SingleLineComment({
     required this.innerContent,
     required this.lineIndex,
     this.characterIndex = 0,
     this.outerContent = '',
+    this.source,
   });
 
   /// Creates the object from [outerContent] by extracting [innerContent]
@@ -28,6 +34,7 @@ class SingleLineComment {
     required int characterIndex,
     required int lineIndex,
     required List<String> sequences,
+    Object? source,
   }) : this(
           characterIndex: characterIndex,
           innerContent: _cutSequence(
@@ -36,6 +43,7 @@ class SingleLineComment {
           ),
           lineIndex: lineIndex,
           outerContent: outerContent,
+          source: source,
         );
 
   @override
@@ -71,3 +79,7 @@ class SingleLineComment {
     throw Exception('$outerContent does not start with any of $sequences');
   }
 }
+
+extension SingleLineCommentIterable on Iterable<SingleLineComment> {
+  Set<Object?> get sources => {...map((e) => e.source)};
+}
diff --git a/lib/src/sizes.dart b/lib/src/sizes.dart
deleted file mode 100644
index 437abdc..0000000
--- a/lib/src/sizes.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-// TODO(nausharipov): where to store constants?
-
-class Sizes {
-  static const double autocompletePopupMaxHeight = 100;
-  static const double autocompletePopupMaxWidth = 300;
-  static const caretPadding = 10;
-}
diff --git a/lib/src/wip/autocomplete/popup.dart b/lib/src/wip/autocomplete/popup.dart
index 091789e..5e2624c 100644
--- a/lib/src/wip/autocomplete/popup.dart
+++ b/lib/src/wip/autocomplete/popup.dart
@@ -1,13 +1,14 @@
+import 'dart:math';
+
 import 'package:flutter/material.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
-import '../../sizes.dart';
 import 'popup_controller.dart';
 
 /// Popup window displaying the list of possible completions
 class Popup extends StatefulWidget {
-  final Offset normalOffset;
-  final Offset flippedOffset;
+  final double row;
+  final double column;
   final Size editingWindowSize;
   final TextStyle style;
   final Color? backgroundColor;
@@ -16,8 +17,8 @@ class Popup extends StatefulWidget {
 
   Popup({
     Key? key,
-    required this.normalOffset,
-    required this.flippedOffset,
+    required this.row,
+    required this.column,
     required this.controller,
     required this.editingWindowSize,
     required this.style,
@@ -26,13 +27,18 @@ class Popup extends StatefulWidget {
   }) : super(key: key);
 
   @override
-  PopupState createState() => PopupState();
+  _PopupState createState() => _PopupState();
 }
 
-class PopupState extends State<Popup> {
+class _PopupState extends State<Popup> {
+  late double width;
+  late double height;
+
   @override
   void initState() {
     widget.controller.addListener(rebuild);
+    this.width = 300;
+    this.height = 100;
     super.initState();
   }
 
@@ -44,40 +50,17 @@ class PopupState extends State<Popup> {
 
   @override
   Widget build(BuildContext context) {
-    final bool verticalOverflow =
-        widget.normalOffset.dy + Sizes.autocompletePopupMaxHeight >
-            widget.editingWindowSize.height;
-    final bool horizontalOverflow =
-        widget.normalOffset.dx + Sizes.autocompletePopupMaxWidth >
-            widget.editingWindowSize.width;
-    final double leftOffsetLimit =
-        // TODO(nausharipov): find where 100 comes from
-        widget.editingWindowSize.width - Sizes.autocompletePopupMaxWidth - 100;
-
-    return Positioned(
-      left: horizontalOverflow ? leftOffsetLimit : widget.normalOffset.dx,
-      top: verticalOverflow ? widget.flippedOffset.dy : widget.normalOffset.dy,
-      child: Container(
-        alignment:
-            verticalOverflow ? Alignment.bottomCenter : Alignment.topCenter,
-        constraints: const BoxConstraints(
-          maxHeight: Sizes.autocompletePopupMaxHeight,
-          maxWidth: Sizes.autocompletePopupMaxWidth,
-        ),
-        // Container is used because the vertical borders
-        // in DecoratedBox are hidden under scroll.
-        // ignore: use_decorated_box
+    return Padding(
+      padding: EdgeInsets.only(
+        left: min(widget.column, widget.editingWindowSize.width - width),
+        top: widget.row,
+      ),
+      child: ConstrainedBox(
+        constraints: BoxConstraints(maxHeight: height, maxWidth: width),
         child: Container(
-          decoration: BoxDecoration(
-            color: widget.backgroundColor,
-            border: Border.all(
-              color: widget.style.color!,
-              width: 0.5,
-            ),
-          ),
           child: ScrollablePositionedList.builder(
             shrinkWrap: true,
-            physics: const ClampingScrollPhysics(),
+            physics: ClampingScrollPhysics(),
             itemScrollController: widget.controller.itemScrollController,
             itemPositionsListener: widget.controller.itemPositionsListener,
             itemCount: widget.controller.suggestions.length,
@@ -85,6 +68,13 @@ class PopupState extends State<Popup> {
               return _buildListItem(index);
             },
           ),
+          decoration: BoxDecoration(
+            color: widget.backgroundColor,
+            border: Border.all(
+              color: widget.style.color!,
+              width: 0.5,
+            ),
+          ),
         ),
       ),
     );
@@ -94,6 +84,19 @@ class PopupState extends State<Popup> {
     return Material(
       color: Colors.grey.withOpacity(0.1),
       child: InkWell(
+        child: Container(
+          child: Padding(
+            padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
+            child: Text(
+              widget.controller.suggestions[index],
+              overflow: TextOverflow.ellipsis,
+              style: widget.style,
+            ),
+          ),
+          color: widget.controller.selectedIndex == index
+              ? Colors.blueAccent.withOpacity(0.5)
+              : Colors.transparent,
+        ),
         onTap: () {
           widget.controller.selectedIndex = index;
           widget.parentFocusNode.requestFocus();
@@ -106,19 +109,6 @@ class PopupState extends State<Popup> {
         hoverColor: Colors.grey.withOpacity(0.1),
         splashColor: Colors.transparent,
         highlightColor: Colors.transparent,
-        child: ColoredBox(
-          color: widget.controller.selectedIndex == index
-              ? Colors.blueAccent.withOpacity(0.5)
-              : Colors.transparent,
-          child: Padding(
-            padding: const EdgeInsets.symmetric(horizontal: 8),
-            child: Text(
-              widget.controller.suggestions[index],
-              overflow: TextOverflow.ellipsis,
-              style: widget.style,
-            ),
-          ),
-        ),
       ),
     );
   }


Test patch:
diff --git a/test/src/folding/parsers/highlight_parser_test.dart b/test/src/folding/parsers/highlight_parser_test.dart
index 9fb69f8..4a97479 100644
--- a/test/src/folding/parsers/highlight_parser_test.dart
+++ b/test/src/folding/parsers/highlight_parser_test.dart
@@ -1,4 +1,8 @@
 import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:flutter_code_editor/src/service_comment_filter/service_comment_filter.dart';
+import 'package:flutter_code_editor/src/single_line_comments/parser/single_line_comment_parser.dart';
+import 'package:flutter_code_editor/src/single_line_comments/parser/single_line_comments.dart';
+import 'package:flutter_code_editor/src/single_line_comments/single_line_comment.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:highlight/highlight.dart';
 import 'package:highlight/languages/java.dart';
@@ -177,6 +181,36 @@ import java.lang.Exception;''',
           ],
           expectedInvalid: const [],
         ),
+
+        _Example(
+          'Java. Comment after a closing brace',
+          code: '''
+class MyClass {
+  void method() {
+  }// comment
+  // comment
+}''',
+          mode: java,
+          expected: const [
+            _FB(startLine: 0, endLine: 4, type: _T.braces),
+            _FB(startLine: 1, endLine: 2, type: _T.braces),
+          ],
+          expectedInvalid: const [],
+        ),
+
+        _Example(
+          'Java. Service comment sequences do not form a foldable block.',
+          code: '''
+class MyClass {
+  // [START section1]
+  // [END section2]
+}''',
+          mode: java,
+          expected: const [
+            _FB(startLine: 0, endLine: 3, type: _T.braces),
+          ],
+          expectedInvalid: const [],
+        ),
       ];
 
       for (final example in examples) {
@@ -184,7 +218,20 @@ import java.lang.Exception;''',
         final highlighted = highlight.parse(example.code, language: 'language');
         final parser = HighlightFoldableBlockParser();
 
-        parser.parse(highlighted);
+        final sequences = SingleLineComments.byMode[example.mode] ?? [];
+
+        final commentParser = SingleLineCommentParser.parseHighlighted(
+          text: example.code,
+          highlighted: highlighted,
+          singleLineCommentSequences: sequences,
+        );
+
+        final serviceComments = ServiceCommentFilter.filter(
+          commentParser.comments,
+          namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        );
+
+        parser.parse(highlighted, serviceComments.sources);
 
         expect(
           parser.blocks,