Repository: akvelon/flutter-code-editor

Issue:
Parse collapsible blocks for Python




Fix patch:
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index a1ea140d..403665ee 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
 import 'package:highlight/highlight_core.dart';
 
 import '../folding/foldable_block.dart';
-import '../folding/parsers/highlight.dart';
+import '../folding/parsers/parser_factory.dart';
 import '../hidden_ranges/hidden_range.dart';
 import '../hidden_ranges/hidden_ranges.dart';
 import '../issues/issue.dart';
@@ -17,9 +17,9 @@ import '../single_line_comments/parser/single_line_comments.dart';
 import '../single_line_comments/single_line_comment.dart';
 import 'code_edit_result.dart';
 import 'code_line.dart';
+import 'code_lines_builder.dart';
 import 'string.dart';
 import 'text_range.dart';
-import 'tokens.dart';
 
 class Code {
   final String text;
@@ -57,11 +57,22 @@ class Code {
     final issues = <Issue>[];
     final List<FoldableBlock> foldableBlocks;
 
-    if (highlighted == null) {
+    final lines = CodeLinesBuilder.textToCodeLines(
+      text: text,
+      readonlyCommentsByLine: commentParser.getIfReadonlyCommentByLine(),
+    );
+
+    if (highlighted == null || language == null) {
       foldableBlocks = const [];
     } else {
-      final parser = HighlightFoldableBlockParser();
-      parser.parse(highlighted, serviceCommentsNodesSet);
+      final parser = FoldableBlockParserFactory.provideParser(language);
+
+      parser.parse(
+        highlighted: highlighted,
+        serviceCommentsSources: serviceCommentsNodesSet,
+        lines: lines,
+      );
+
       foldableBlocks = parser.blocks;
       issues.addAll(parser.invalidBlocks.map((b) => b.issue));
     }
@@ -72,13 +83,6 @@ 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,
@@ -128,59 +132,6 @@ class Code {
     visibleText: '',
   );
 
-  static List<CodeLine> _textToCodeLines({
-    required String text,
-    required Result? highlighted,
-    required Mode? language,
-    required Map<int, SingleLineComment> commentsByLines,
-  }) {
-    final result = <CodeLine>[];
-    final lines = text.split('\n');
-    int charIndex = 0;
-
-    int lineIndex = 0;
-    final lastLineIndex = lines.length - 1;
-
-    for (final line in lines) {
-      final comment = commentsByLines[lineIndex];
-      final words = _getCommentWords(comment?.innerContent);
-
-      String lineText = '$line\n';
-      bool isReadOnly = words.contains(Tokens.readonly);
-
-      if (lineIndex == lastLineIndex) {
-        // The last line is special. It has no newline at the end.
-        // If it's empty, it inherits isReadOnly from the previous line.
-        // Otherwise, if we wanted a read-only document end, we could
-        // not use newline at the end of it as POSIX requires.
-        lineText = line;
-        if (line == '') {
-          isReadOnly = result.lastOrNull?.isReadOnly ?? false;
-        }
-      }
-
-      result.add(
-        CodeLine.fromTextAndStart(
-          lineText,
-          charIndex,
-          isReadOnly: isReadOnly,
-        ),
-      );
-
-      charIndex += line.length + 1;
-      lineIndex++;
-    }
-
-    return result;
-  }
-
-  static List<String> _getCommentWords(
-    String? comment,
-  ) {
-    // Split by any whitespaces.
-    return comment?.split(RegExp(r'\s+')) ?? const <String>[];
-  }
-
   static void _applyNamedSectionsToLines({
     required List<CodeLine> lines,
     required Map<String, NamedSection> sections,
diff --git a/lib/src/code/code_line.dart b/lib/src/code/code_line.dart
index de31b6ab..8884f828 100644
--- a/lib/src/code/code_line.dart
+++ b/lib/src/code/code_line.dart
@@ -1,5 +1,6 @@
 import 'dart:ui';
 
+import 'package:charcode/ascii.dart';
 import 'package:meta/meta.dart';
 
 @immutable
@@ -8,8 +9,9 @@ class CodeLine {
 
   final TextRange textRange;
   final bool isReadOnly;
+  late final indent = _calculateIndent();
 
-  const CodeLine({
+  CodeLine({
     required this.text,
     required this.textRange,
     this.isReadOnly = false,
@@ -47,4 +49,17 @@ class CodeLine {
         textRange,
         isReadOnly,
       );
+
+  int _calculateIndent() {
+    int indentation = 0;
+    for (final character in text.runes) {
+      if (character == $space || character == $tab || character == $lf) {
+        indentation++;
+      } else {
+        break;
+      }
+    }
+
+    return indentation;
+  }
 }
diff --git a/lib/src/code/code_lines_builder.dart b/lib/src/code/code_lines_builder.dart
new file mode 100644
index 00000000..bc4409ae
--- /dev/null
+++ b/lib/src/code/code_lines_builder.dart
@@ -0,0 +1,61 @@
+import 'package:collection/collection.dart';
+
+import 'code_line.dart';
+
+class CodeLinesBuilder {
+  static List<CodeLine> textToCodeLines({
+    required String text,
+    required Map<int, bool> readonlyCommentsByLine,
+  }) {
+    final result = <CodeLine>[];
+    final lines = _splitText(text);
+    int charIndex = 0;
+
+    for (int lineIndex = 0; lineIndex < lines.length; lineIndex++) {
+      final line = lines[lineIndex];
+      final isReadOnly = _isLineReadonly(
+        containsReadonlyComment: readonlyCommentsByLine[lineIndex],
+        isLastLine: lineIndex == lines.length - 1,
+        isEmptyLine: line.isEmpty,
+        isPreviousLineReadonly: result.lastOrNull?.isReadOnly,
+      );
+
+      result.add(
+        CodeLine.fromTextAndStart(line, charIndex, isReadOnly: isReadOnly),
+      );
+
+      charIndex += line.length;
+    }
+
+    return result;
+  }
+
+  static List<String> _splitText(String text) {
+    final lines = text.split('\n');
+    final result = <String>[];
+    for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) {
+      final line = lines[lineIndex];
+      final isLastLine = lineIndex == lines.length - 1;
+      final lineText = isLastLine ? line : '$line\n';
+      result.add(lineText);
+    }
+    return result;
+  }
+
+  static bool _isLineReadonly({
+    required bool? containsReadonlyComment,
+    required bool isLastLine,
+    required bool isEmptyLine,
+    required bool? isPreviousLineReadonly,
+  }) {
+    bool isReadOnly = containsReadonlyComment ?? false;
+
+    // If last line is empty, it inherits isReadOnly from the previous line.
+    // Otherwise, if we wanted a read-only document end, we could
+    // not use newline at the end of it as POSIX requires.
+    if (isLastLine && isEmptyLine) {
+      isReadOnly = isPreviousLineReadonly ?? false;
+    }
+    return isReadOnly;
+  }
+}
diff --git a/lib/src/folding/foldable_block.dart b/lib/src/folding/foldable_block.dart
index 60bd2cff..51f55322 100644
--- a/lib/src/folding/foldable_block.dart
+++ b/lib/src/folding/foldable_block.dart
@@ -19,6 +19,10 @@ class FoldableBlock with EquatableMixin {
         endLine,
         type,
       ];
+
+  bool includes(FoldableBlock other) {
+    return startLine <= other.startLine && endLine >= other.endLine;
+  }
 }
 
 extension FoldableBlockList on List<FoldableBlock> {
diff --git a/lib/src/folding/foldable_block_type.dart b/lib/src/folding/foldable_block_type.dart
index c9629858..75bf9653 100644
--- a/lib/src/folding/foldable_block_type.dart
+++ b/lib/src/folding/foldable_block_type.dart
@@ -8,6 +8,9 @@ enum FoldableBlockType {
   /// ( )
   parentheses,
 
+  /// Spaces or tabs.
+  indent,
+
   multilineComment,
   singleLineComment,
   imports,
diff --git a/lib/src/folding/parsers/abstract.dart b/lib/src/folding/parsers/abstract.dart
index 08989414..ada51f6a 100644
--- a/lib/src/folding/parsers/abstract.dart
+++ b/lib/src/folding/parsers/abstract.dart
@@ -1,6 +1,8 @@
 import 'package:collection/collection.dart';
+import 'package:highlight/highlight_core.dart';
 import 'package:meta/meta.dart';
 
+import '../../code/code_line.dart';
 import '../foldable_block.dart';
 import '../foldable_block_type.dart';
 import '../invalid_foldable_block.dart';
@@ -10,7 +12,7 @@ import 'line_semantics.dart';
 /// of code to parse blocks from it.
 ///
 /// This object accumulates data in its fields as the parsing progresses.
-class AbstractFoldableBlockParser {
+abstract class AbstractFoldableBlockParser {
   /// Valid blocks by the end of the parsing.
   final blocks = <FoldableBlock>[];
 
@@ -25,6 +27,12 @@ class AbstractFoldableBlockParser {
   /// Used when grouping sequential imports and single line comments.
   final _linesWithSemantics = <_LineWithSemantics>[];
 
+  void parse({
+    required Result highlighted,
+    required Set<Object?> serviceCommentsSources,
+    required List<CodeLine> lines,
+  });
+
   /// Records that a block has started at [line].
   @protected
   void startBlock(int line, FoldableBlockType type) {
diff --git a/lib/src/folding/parsers/highlight.dart b/lib/src/folding/parsers/highlight.dart
index 7f178e4c..bc10a95b 100644
--- a/lib/src/folding/parsers/highlight.dart
+++ b/lib/src/folding/parsers/highlight.dart
@@ -2,6 +2,7 @@ import 'package:charcode/ascii.dart';
 import 'package:collection/collection.dart';
 import 'package:highlight/highlight_core.dart';
 
+import '../../code/code_line.dart';
 import '../../highlight/keyword_semantics.dart';
 import '../../highlight/node.dart';
 import '../../highlight/node_classes.dart';
@@ -31,7 +32,12 @@ class HighlightFoldableBlockParser extends AbstractFoldableBlockParser {
   /// not a comment.
   bool _foundNonWhitespace = false;
 
-  void parse(Result highlighted, Set<Object?> serviceCommentsSources) {
+  @override
+  void parse({
+    required Result highlighted,
+    required Set<Object?> serviceCommentsSources,
+    List<CodeLine> lines = const [],
+  }) {
     if (highlighted.nodes != null) {
       _processNodes(highlighted.nodes!, serviceCommentsSources);
     }
diff --git a/lib/src/folding/parsers/indent.dart b/lib/src/folding/parsers/indent.dart
new file mode 100644
index 00000000..d6e7cf49
--- /dev/null
+++ b/lib/src/folding/parsers/indent.dart
@@ -0,0 +1,176 @@
+import 'package:highlight/highlight_core.dart';
+
+import '../../code/code_line.dart';
+import '../foldable_block.dart';
+import '../foldable_block_type.dart';
+import 'abstract.dart';
+
+/// A parser for foldable blocks from lines indentation.
+class IndentFoldableBlockParser extends AbstractFoldableBlockParser {
+  final _openBlocksLinesByIndent = <int, int>{};
+  List<int?> _linesIndents = [];
+
+  @override
+  void parse({
+    Result? highlighted,
+    Set<Object?> serviceCommentsSources = const {},
+    required List<CodeLine> lines,
+  }) {
+    _parse(lines);
+    finalize();
+  }
+
+  void _parse(List<CodeLine> lines) {
+    _linesIndents = _calculateLinesIndents(lines);
+    final significantIndentIndexes =
+        _SignificantIndentIndexes.fromLineIndents(_linesIndents);
+
+    if (significantIndentIndexes == null) {
+      return;
+    }
+
+    _createBlocks(significantIndentIndexes);
+    _closeAllOpenedBlocksAt(significantIndentIndexes.last);
+
+    _openBlocksLinesByIndent.clear();
+    _linesIndents = [];
+  }
+
+  List<int?> _calculateLinesIndents(List<CodeLine> lines) {
+    final result = List<int?>.filled(lines.length, 0);
+    for (int i = 0; i < lines.length; i++) {
+      final line = lines[i];
+      final isSeparatorLine = line.indent == line.text.length;
+      result[i] = isSeparatorLine ? null : line.indent;
+    }
+    return result;
+  }
+
+  void _createBlocks(_SignificantIndentIndexes significantIndentIndexes) {
+    int lastExistingIndent = _linesIndents[significantIndentIndexes.first]!;
+    int lastExistingIndentIndex = significantIndentIndexes.first;
+
+    for (int i = significantIndentIndexes.second;
+        i < _linesIndents.length;
+        i++) {
+      final currentLineIndent = _linesIndents[i];
+
+      if (_isSeparatorLine(currentLineIndent)) {
+        continue;
+      }
+
+      if (currentLineIndent! > lastExistingIndent) {
+        _openBlock(lastExistingIndent, lastExistingIndentIndex);
+      } else {
+        _closeBlocks(lastExistingIndentIndex, currentLineIndent);
+      }
+      lastExistingIndentIndex = i;
+      lastExistingIndent = currentLineIndent;
+    }
+  }
+
+  void _closeAllOpenedBlocksAt(int index) {
+    _openBlocksLinesByIndent.forEach((indentsCount, startLine) {
+      _closeBlock(startLine, index);
+    });
+  }
+
+  bool _isSeparatorLine(int? indent) => indent == null;
+
+  void _openBlock(int indent, int lineIndex) {
+    _openBlocksLinesByIndent[indent] = lineIndex;
+  }
+
+  void _closeBlocks(int i, int nextNonEmptyIndent) {
+    _openBlocksLinesByIndent.forEachInvertedWhile(
+      (indentsCount, startLine) {
+        _closeBlock(startLine, i);
+      },
+      executeWhile: (indentsCount, _) => indentsCount >= nextNonEmptyIndent,
+    );
+    _openBlocksLinesByIndent.removeWhere(
+      (key, value) => key >= nextNonEmptyIndent,
+    );
+  }
+
+  void _closeBlock(int startLine, int endLine) {
+    blocks.add(
+      FoldableBlock(
+        type: FoldableBlockType.indent,
+        startLine: startLine,
+        endLine: endLine,
+      ),
+    );
+  }
+}
+
+class _SignificantIndentIndexes {
+  final int first;
+  final int second;
+  final int last;
+
+  _SignificantIndentIndexes(this.first, this.second, this.last);
+
+  static _SignificantIndentIndexes? fromLineIndents(List<int?> linesIndents) {
+    final first = _getNextSignificantIndentIndex(linesIndents);
+    if (first == null) {
+      return null;
+    }
+
+    final second = _getNextSignificantIndentIndex(
+      linesIndents,
+      startIndex: first + 1,
+    );
+    if (second == null) {
+      return null;
+    }
+
+    final last = _getLastSignificantIndentIndex(linesIndents);
+    if (last == null) {
+      return null;
+    }
+
+    return _SignificantIndentIndexes(first, second, last);
+  }
+
+  static int? _getNextSignificantIndentIndex(
+      List<int?> indents, {
+        int startIndex = 0,
+      }) {
+    for (int i = startIndex; i < indents.length; i++) {
+      if (!_isSeparatorLine(indents[i])) {
+        return i;
+      }
+    }
+    return null;
+  }
+
+  static int? _getLastSignificantIndentIndex(List<int?> indents) {
+    for (int i = indents.length - 1; i >= 0; i--) {
+      if (!_isSeparatorLine(indents[i])) {
+        return i;
+      }
+    }
+    return null;
+  }
+
+  static bool _isSeparatorLine(int? indent) => indent == null;
+}
+
+extension _MyMap<K, V> on Map<K, V> {
+  /// Iterates over the map in reverse order while [executeWhile] returns true.
+  void forEachInvertedWhile(
+    void Function(K key, V value) f, {
+    required bool Function(K key, V value) executeWhile,
+  }) {
+    final keys = this.keys.toList();
+    for (int i = keys.length - 1; i >= 0; i--) {
+      if (!executeWhile(keys[i], this[keys[i]]!)) {
+        break;
+      }
+      final key = keys[i];
+      final value = this[key];
+      f(key, value!);
+    }
+  }
+}
diff --git a/lib/src/folding/parsers/parser_factory.dart b/lib/src/folding/parsers/parser_factory.dart
new file mode 100644
index 00000000..700ca8f8
--- /dev/null
+++ b/lib/src/folding/parsers/parser_factory.dart
@@ -0,0 +1,15 @@
+import 'package:highlight/highlight_core.dart';
+import 'package:highlight/languages/python.dart';
+
+import 'abstract.dart';
+import 'highlight.dart';
+import 'python.dart';
+
+class FoldableBlockParserFactory {
+  static AbstractFoldableBlockParser provideParser(Mode mode) {
+    if (mode == python) {
+      return PythonFoldableBlockParser();
+    }
+    return HighlightFoldableBlockParser();
+  }
+}
diff --git a/lib/src/folding/parsers/python.dart b/lib/src/folding/parsers/python.dart
new file mode 100644
index 00000000..06140466
--- /dev/null
+++ b/lib/src/folding/parsers/python.dart
@@ -0,0 +1,140 @@
+import 'package:highlight/highlight_core.dart';
+
+import '../../code/code_line.dart';
+import '../foldable_block.dart';
+import 'abstract.dart';
+import 'highlight.dart';
+import 'indent.dart';
+
+///A parser for foldable blocks for python
+class PythonFoldableBlockParser extends AbstractFoldableBlockParser {
+  @override
+  List<FoldableBlock> parse({
+    required Result highlighted,
+    required Set<Object?> serviceCommentsSources,
+    required List<CodeLine> lines,
+  }) {
+    final highlightBlocks = _getBlocksFromParser(
+      HighlightFoldableBlockParser(),
+      highlighted,
+      serviceCommentsSources,
+      lines,
+    );
+    final indentBlocks = _getBlocksFromParser(
+      IndentFoldableBlockParser(),
+      highlighted,
+      serviceCommentsSources,
+      lines,
+    );
+
+    if (indentBlocks.isEmpty) {
+      return highlightBlocks;
+    }
+
+    return _combineBlocks(
+      highlightBlocks: highlightBlocks,
+      indentBlocks: indentBlocks,
+    );
+  }
+
+  List<FoldableBlock> _getBlocksFromParser(
+    AbstractFoldableBlockParser parser,
+    Result highlighted,
+    Set<Object?> serviceCommentsSources,
+    List<CodeLine> lines,
+  ) {
+    parser.parse(
+      highlighted: highlighted,
+      serviceCommentsSources: serviceCommentsSources,
+      lines: lines,
+    );
+    invalidBlocks.addAll(parser.invalidBlocks);
+    return parser.blocks;
+  }
+
+  /// Compares two lists of blocks and combines them into one list
+  /// with priority to highlight blocks if blocks intersect.
+  ///
+  /// Lets consider this python code for example:
+  /// a = [     # 0
+  ///     1,    # 1
+  ///     2,    # 2
+  ///     3,    # 3
+  ///     4,    # 4
+  ///  ]        # 5
+  /// Highlight block will return [0, 5].
+  /// Indent block will return [0, 4] block.
+  /// We need to skip [0, 4] block because it is inside [0, 5] block.
+  List<FoldableBlock> _combineBlocks({
+    required List<FoldableBlock> highlightBlocks,
+    required List<FoldableBlock> indentBlocks,
+  }) {
+    int highlightBlockIndex = 0;
+    int indentBlockIndex = 0;
+
+    final result = <FoldableBlock>[];
+
+    while (highlightBlockIndex < highlightBlocks.length ||
+        indentBlockIndex < indentBlocks.length) {
+      if (highlightBlockIndex >= highlightBlocks.length) {
+        result.addAll(indentBlocks.skip(indentBlockIndex));
+        break;
+      }
+
+      final highlightBlock = highlightBlocks[highlightBlockIndex];
+
+      indentBlockIndex = _addAllPossibleIndentBlocks(
+        indentBlockIndex,
+        indentBlocks,
+        highlightBlock.startLine,
+        result,
+      );
+
+      while (indentBlockIndex < indentBlocks.length &&
+          highlightBlock.includes(indentBlocks[indentBlockIndex])) {
+        indentBlockIndex++;
+      }
+
+      result.add(highlightBlock);
+      highlightBlockIndex++;
+    }
+
+    return result;
+  }
+
+  int _addAllPossibleIndentBlocks(
+    int indentBlockIndex,
+    List<FoldableBlock> indentBlocks,
+    int highlightBlockStartLine,
+    List<FoldableBlock> result,
+  ) {
+    final indentBlocksBeforeHighlight = _getBlocksCountBeforeLineFrom(
+      startIndex: indentBlockIndex,
+      line: highlightBlockStartLine,
+      blocks: indentBlocks,
+    );
+    final possibleToAddBlocks = indentBlocks.sublist(
+      indentBlockIndex,
+      indentBlockIndex + indentBlocksBeforeHighlight,
+    );
+    result.addAll(possibleToAddBlocks);
+    return indentBlockIndex + indentBlocksBeforeHighlight;
+  }
+
+  int _getBlocksCountBeforeLineFrom({
+    required int startIndex,
+    required int line,
+    required List<FoldableBlock> blocks,
+  }) {
+    int result = 0;
+    for (int i = startIndex; i < blocks.length; i++) {
+      final indentBlock = blocks[i];
+      if (indentBlock.startLine < line) {
+        result++;
+      } else {
+        break;
+      }
+    }
+    return result;
+  }
+}
diff --git a/lib/src/service_comment_filter/service_comment_filter.dart b/lib/src/service_comment_filter/service_comment_filter.dart
index f29d0647..bfa40e0d 100644
--- a/lib/src/service_comment_filter/service_comment_filter.dart
+++ b/lib/src/service_comment_filter/service_comment_filter.dart
@@ -17,8 +17,7 @@ class ServiceCommentFilter {
     SingleLineComment comment, {
     required AbstractNamedSectionParser? namedSectionParser,
   }) {
-    final words = _getCommentWords(comment.innerContent);
-    if (words.contains(Tokens.readonly)) {
+    if (comment.isReadonly) {
       return true;
     }
 
@@ -33,11 +32,4 @@ class ServiceCommentFilter {
 
     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/abstract_single_line_comment_parser.dart b/lib/src/single_line_comments/parser/abstract_single_line_comment_parser.dart
index 57a29c04..bc2c6342 100644
--- a/lib/src/single_line_comments/parser/abstract_single_line_comment_parser.dart
+++ b/lib/src/single_line_comments/parser/abstract_single_line_comment_parser.dart
@@ -5,4 +5,13 @@ abstract class AbstractSingleLineCommentParser {
 
   Map<int, SingleLineComment> getCommentsByLines() =>
       {for (final comment in comments) comment.lineIndex: comment};
+
+
+  Map<int, bool> getIfReadonlyCommentByLine() {
+    final result = <int, bool>{};
+    for (final comment in comments) {
+      result[comment.lineIndex] = comment.isReadonly;
+    }
+    return result;
+  }
 }
diff --git a/lib/src/single_line_comments/single_line_comment.dart b/lib/src/single_line_comments/single_line_comment.dart
index 1cc74580..de8e5bd0 100644
--- a/lib/src/single_line_comments/single_line_comment.dart
+++ b/lib/src/single_line_comments/single_line_comment.dart
@@ -1,5 +1,7 @@
 import 'package:meta/meta.dart';
 
+import '../code/tokens.dart';
+
 @immutable
 class SingleLineComment {
   /// Zero-based index of the first character from the beginning of the text.
@@ -19,7 +21,9 @@ class SingleLineComment {
   /// The object class is specific to the parser.
   final Object? source;
 
-  const SingleLineComment({
+  late final isReadonly = _checkIfReadonly();
+
+  SingleLineComment({
     required this.innerContent,
     required this.lineIndex,
     this.characterIndex = 0,
@@ -66,6 +70,11 @@ class SingleLineComment {
   @override
   String toString() => 'Line $lineIndex: "$outerContent"';
 
+  bool _checkIfReadonly() {
+    final words = innerContent.split(RegExp(r'\s+'));
+    return words.contains(Tokens.readonly);
+  }
+
   static String _cutSequence({
     required String outerContent,
     required List<String> sequences,


Test patch:
diff --git a/test/src/folding/parsers/highlight_parser_test.dart b/test/src/folding/parsers/highlight_parser_test.dart
index 4a974798..facb4e57 100644
--- a/test/src/folding/parsers/highlight_parser_test.dart
+++ b/test/src/folding/parsers/highlight_parser_test.dart
@@ -1,10 +1,14 @@
-import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block_type.dart';
+import 'package:flutter_code_editor/src/folding/invalid_foldable_block.dart';
+import 'package:flutter_code_editor/src/folding/parsers/highlight.dart';
+import 'package:flutter_code_editor/src/named_sections/parsers/brackets_start_end.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/highlight_core.dart';
 import 'package:highlight/languages/java.dart';
 
 void main() {
@@ -231,7 +235,10 @@ class MyClass {
           namedSectionParser: const BracketsStartEndNamedSectionParser(),
         );
 
-        parser.parse(highlighted, serviceComments.sources);
+        parser.parse(
+          highlighted: highlighted,
+          serviceCommentsSources: serviceComments.sources,
+        );
 
         expect(
           parser.blocks,
diff --git a/test/src/folding/parsers/indent_parser_test.dart b/test/src/folding/parsers/indent_parser_test.dart
new file mode 100644
index 00000000..7924170f
--- /dev/null
+++ b/test/src/folding/parsers/indent_parser_test.dart
@@ -0,0 +1,218 @@
+import 'package:flutter_code_editor/src/code/code_lines_builder.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block_type.dart';
+import 'package:flutter_code_editor/src/folding/parsers/indent.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_test/flutter_test.dart';
+import 'package:highlight/highlight_core.dart';
+import 'package:highlight/languages/python.dart';
+
+void main() {
+  group('IndentFoldableBlockParser', () {
+    test('parses indents', () {
+      const examples = [
+        //
+        _Example(
+          'Python. Empty text',
+          code: '',
+          expected: [],
+        ),
+
+        _Example(
+          'Python. One nesting',
+          code: '''
+class Mapping:
+    def __init__(self, iterable):
+        self.items_list = []
+        self.__update(iterable)''',
+          expected: [
+            _FB(startLine: 0, endLine: 3, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Several nesting with one space indent',
+          code: '''
+class Mapping:
+ def __init__(self, iterable):
+  self.items_list = []
+  self.__update(iterable)
+
+ def update(self, iterable):
+  for item in iterable:
+   self.items_list.append(item)''',
+          expected: [
+            _FB(startLine: 0, endLine: 7, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+            _FB(startLine: 5, endLine: 7, type: _T.indent),
+            _FB(startLine: 6, endLine: 7, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Several separators at the mid',
+          code: '''
+class Mapping:
+
+    def __init__(self, iterable):
+
+        self.items_list = []
+
+        self.__update(iterable)
+
+
+    def update(self, iterable):
+
+
+        for item in iterable:
+
+            self.items_list.append(item)''',
+          expected: [
+            _FB(startLine: 0, endLine: 14, type: _T.indent),
+            _FB(startLine: 2, endLine: 6, type: _T.indent),
+            _FB(startLine: 9, endLine: 14, type: _T.indent),
+            _FB(startLine: 12, endLine: 14, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Several separators at start and end',
+          code: '''
+
+
+class Mapping:
+    def __init__(self, iterable):
+        self.items_list = []
+        self.__update(iterable)
+
+    def update(self, iterable):
+        for item in iterable:
+            self.items_list.append(item)
+            
+            
+''',
+          expected: [
+            _FB(startLine: 2, endLine: 9, type: _T.indent),
+            _FB(startLine: 3, endLine: 5, type: _T.indent),
+            _FB(startLine: 7, endLine: 9, type: _T.indent),
+            _FB(startLine: 8, endLine: 9, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Without separator lines',
+          code: '''
+class Mapping:
+    def __init__(self, iterable):
+        self.items_list = []
+        self.__update(iterable)
+class Foo:
+    def update(self, iterable):
+        for item in iterable:
+            self.items_list.append(item)''',
+          expected: [
+            _FB(startLine: 0, endLine: 3, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+            _FB(startLine: 4, endLine: 7, type: _T.indent),
+            _FB(startLine: 5, endLine: 7, type: _T.indent),
+            _FB(startLine: 6, endLine: 7, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Invalid indent',
+          code: '''
+class Mapping:
+    def __init__(self, iterable):
+        self.items_list = []
+        self.__update(iterable)
+     def update(self, iterable):
+         for item in iterable:
+        self.items_list.append(item)''',
+          expected: [
+            _FB(startLine: 0, endLine: 6, type: _T.indent),
+            _FB(startLine: 1, endLine: 6, type: _T.indent),
+            _FB(startLine: 4, endLine: 6, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Different indents',
+          code: '''
+class Mapping:                                # 0
+ def __init__(self, iterable):                # 1
+        self.items_list = []                  # 2
+        self.__update(iterable)               # 3
+
+ def update(self, iterable):                  # 5
+      for item in iterable:                   # 6
+        self.items_list.append(item)          # 7''',
+          expected: [
+            _FB(startLine: 0, endLine: 7, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+            _FB(startLine: 5, endLine: 7, type: _T.indent),
+            _FB(startLine: 6, endLine: 7, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Multiline list',
+          code: '''
+numbers = [1, 
+    2, 
+    3, 
+    4, 
+    5
+]''',
+          expected: [_FB(startLine: 0, endLine: 4, type: _T.indent)],
+        )
+      ];
+
+      for (final example in examples) {
+        highlight.registerLanguage('language', python);
+        final highlighted = highlight.parse(example.code, language: 'language');
+
+        final sequences = SingleLineComments.byMode[python] ?? [];
+
+        final commentParser = SingleLineCommentParser.parseHighlighted(
+          text: example.code,
+          highlighted: highlighted,
+          singleLineCommentSequences: sequences,
+        );
+
+        final codeLines = CodeLinesBuilder.textToCodeLines(
+          text: example.code,
+          readonlyCommentsByLine: commentParser.getIfReadonlyCommentByLine(),
+        );
+
+        final parser = IndentFoldableBlockParser();
+        parser.parse(lines: codeLines);
+        expect(
+          parser.blocks,
+          example.expected,
+          reason: '${example.name}, valid blocks',
+        );
+      }
+    });
+  });
+}
+
+class _Example {
+  final String name;
+  final String code;
+  final List<FoldableBlock> expected;
+
+  const _Example(
+    this.name, {
+    required this.code,
+    required this.expected,
+  });
+}
+
+/// Shorter alias for [FoldableBlock] to avoid line breaks.
+typedef _FB = FoldableBlock;
+
+/// Shorter alias for [FoldableBlockType] to avoid line breaks.
+typedef _T = FoldableBlockType;
diff --git a/test/src/folding/parsers/python_parser_test.dart b/test/src/folding/parsers/python_parser_test.dart
new file mode 100644
index 00000000..b3ef89a5
--- /dev/null
+++ b/test/src/folding/parsers/python_parser_test.dart
@@ -0,0 +1,301 @@
+import 'package:flutter_code_editor/src/code/code_lines_builder.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block.dart';
+import 'package:flutter_code_editor/src/folding/foldable_block_type.dart';
+import 'package:flutter_code_editor/src/folding/parsers/python.dart';
+import 'package:flutter_code_editor/src/named_sections/parsers/brackets_start_end.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_test/flutter_test.dart';
+import 'package:highlight/highlight_core.dart';
+import 'package:highlight/languages/python.dart';
+
+void main() {
+  group('Python. Foldable blocks', () {
+    test('parses spaces', () {
+      const examples = [
+        //
+        _Example(
+          'Python. Empty string',
+          code: '',
+          expected: [],
+        ),
+
+        _Example(
+          'Python. Nesting with multiline list',
+          code: '''
+class Mapping:                      # 0
+ def __init__(self, iterable):      # 1
+  self.items_list = []              # 2
+  self.__update(iterable)           # 3
+
+ def update(self, iterable):        # 5
+  for item in iterable:             # 6
+    self.items_list.append(item)    # 7
+
+  a = [                             # 9
+       5,                           # 10
+       6,                           # 11
+       7,                           # 12
+       8                            # 13
+  ]                                 # 14''',
+          expected: [
+            _FB(startLine: 0, endLine: 14, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+            _FB(startLine: 5, endLine: 14, type: _T.indent),
+            _FB(startLine: 6, endLine: 7, type: _T.indent),
+            _FB(startLine: 9, endLine: 14, type: _T.brackets),
+          ],
+        ),
+
+        _Example(
+          '''Python. Invalid code. Removed ':' symbols''',
+          code: '''
+class Mapping:                                   # 0
+    def __init__(self, iterable)                 # 1
+        self.items_list = []                     # 2
+        self.__update(iterable)                  # 3
+
+    def update(self, iterable)                   # 5
+        for item in iterable                     # 6
+            self.items_list.append(item)         # 7
+
+        a = [                                    # 9 
+            5,                                   # 10
+            6,                                   # 11
+            7,                                   # 12
+            8,
+        ]                                        # 14''',
+          expected: [
+            _FB(startLine: 0, endLine: 14, type: _T.indent),
+            _FB(startLine: 1, endLine: 3, type: _T.indent),
+            _FB(startLine: 5, endLine: 14, type: _T.indent),
+            _FB(startLine: 6, endLine: 7, type: _T.indent),
+            _FB(startLine: 9, endLine: 14, type: _T.brackets),
+          ],
+        ),
+
+        _Example(
+          'Python. Nesting list',
+          code: '''
+class Mapping:                               # 0
+    def __init__(self, iterable):            # 1
+        a = [                                # 2
+            [                                # 3
+                5,                           # 4
+                6,                           # 5
+            ],                               # 6
+            [                                # 7
+                7,                           # 8
+                8,                           # 9
+            ],                               # 10
+            ]                                # 11''',
+          expected: [
+            _FB(startLine: 0, endLine: 11, type: _T.indent),
+            _FB(startLine: 1, endLine: 11, type: _T.indent),
+            _FB(startLine: 2, endLine: 11, type: _T.brackets),
+            _FB(startLine: 3, endLine: 6, type: _T.brackets),
+            _FB(startLine: 7, endLine: 10, type: _T.brackets),
+          ],
+        ),
+
+        _Example(
+          'Python. Single-line comments',
+          code: '''
+class Mapping:                               # 0
+    def __init__(self, iterable):            # 1
+        a = [                                # 2
+            [                                # 3
+              # comment                        4
+              # another comment                5
+              # third comment                  6
+                5,                           # 7
+                6,                           # 8
+            ],                               # 9
+            [                                # 10
+                7,                           # 11
+                8                            # 12
+            ],                               # 13
+            [                                # 14
+                9,                           # 15
+                10                           # 16
+                  # comment                    17
+                  # another comment            18
+                  # third comment              19
+            ],                               # 20
+        ]                                    # 21''',
+          expected: [
+            _FB(startLine: 0, endLine: 21, type: _T.indent),
+            _FB(startLine: 1, endLine: 21, type: _T.indent),
+            _FB(startLine: 2, endLine: 21, type: _T.brackets),
+            _FB(startLine: 3, endLine: 9, type: _T.brackets),
+            _FB(startLine: 4, endLine: 6, type: _T.singleLineComment),
+            _FB(startLine: 10, endLine: 13, type: _T.brackets),
+            _FB(startLine: 14, endLine: 20, type: _T.brackets),
+            _FB(startLine: 17, endLine: 19, type: _T.singleLineComment),
+          ],
+        ),
+
+        _Example(
+          'Python. Pair characters in literals are ignored',
+          code: '''
+a = '{[(';
+b = ")]}";
+''',
+          expected: [],
+        ),
+
+        _Example(
+          'Python. Service comments do not form blocks',
+          code: '''
+class Mapping:
+    def __init__(self, iterable):
+        # [START section1]
+        # [END section1]
+        a = 5''',
+          expected: [
+            _FB(startLine: 0, endLine: 4, type: _T.indent),
+            _FB(startLine: 1, endLine: 4, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. One-liners expressions do not form blocks',
+          code: '''
+def printAgeType(age):                         # 0
+    type = "Minor" if age < 18 else "Adult"    # 1
+    print(type)                                # 2
+
+def squareNumbers(numbers):                    # 4
+    squaredNumbers = [x**2 for x in numbers]   # 5
+    print(squaredNumbers)                      # 6
+
+printAgeType(19)                               # 8
+squareNumbers([1, 2, 3, 4, 5])                 # 9''',
+          expected: [
+            _FB(startLine: 0, endLine: 2, type: _T.indent),
+            _FB(startLine: 4, endLine: 6, type: _T.indent),
+          ],
+        ),
+
+        _Example(
+          'Python. Return only highlight blocks if it does not contain indents',
+          code: '''
+numbers = [1, 
+2,
+3, 
+4, 
+5
+]
+squaredNumbers = [x**2 for x in numbers]
+print(squaredNumbers)''',
+          expected: [_FB(startLine: 0, endLine: 5, type: _T.brackets)],
+        ),
+
+        _Example(
+          'Python. Imports form foldable block',
+          code: '''
+import math
+from math import sqrt
+from flutter import dart as language
+
+import foo
+import bar
+
+pie = math.pi
+print("The value of pi is : ",pie)''',
+          expected: [
+            _FB(startLine: 0, endLine: 5, type: _T.imports),
+          ],
+        ),
+
+        _Example(
+          'Python. Single line comments at start of file',
+          code: '''
+# This is a comment                   # 0
+# This is another comment             # 1
+# This is a third comment             # 2
+
+import math                           # 4
+from math import sqrt                 # 5
+from flutter import dart as language  # 6
+import foo                            # 7
+import bar                            # 8
+
+pie = math.pi                         # 10
+print("The value of pi is : ",pie)    # 11''',
+          expected: [
+            _FB(startLine: 0, endLine: 2, type: _T.singleLineComment),
+            _FB(startLine: 4, endLine: 8, type: _T.imports),
+          ],
+        ),
+
+        _Example(
+          'Python. Nested highlight blocks',
+          code: '''
+a = [[
+  5
+  ]
+]''',
+          expected: [
+            _FB(startLine: 0, endLine: 2, type: _T.brackets),
+            _FB(startLine: 0, endLine: 3, type: _T.brackets),
+          ],
+        ),
+      ];
+
+      for (final example in examples) {
+        highlight.registerLanguage('language', python);
+        final highlighted = highlight.parse(example.code, language: 'language');
+
+        final sequences = SingleLineComments.byMode[python] ?? [];
+
+        final commentParser = SingleLineCommentParser.parseHighlighted(
+          text: example.code,
+          highlighted: highlighted,
+          singleLineCommentSequences: sequences,
+        );
+
+        final serviceComments = ServiceCommentFilter.filter(
+          commentParser.comments,
+          namedSectionParser: const BracketsStartEndNamedSectionParser(),
+        );
+
+        final codeLines = CodeLinesBuilder.textToCodeLines(
+          text: example.code,
+          readonlyCommentsByLine: commentParser.getIfReadonlyCommentByLine(),
+        );
+
+        final pythonParser = PythonFoldableBlockParser().parse(
+          highlighted: highlighted,
+          serviceCommentsSources: serviceComments.map((e) => e.source).toSet(),
+          lines: codeLines,
+        );
+
+        expect(
+          pythonParser,
+          example.expected,
+          reason: '${example.name}, valid blocks',
+        );
+      }
+    });
+  });
+}
+
+class _Example {
+  final String name;
+  final String code;
+  final List<FoldableBlock> expected;
+
+  const _Example(
+    this.name, {
+    required this.code,
+    required this.expected,
+  });
+}
+
+/// Shorter alias for [FoldableBlock] to avoid line breaks.
+typedef _FB = FoldableBlock;
+
+/// Shorter alias for [FoldableBlockType] to avoid line breaks.
+typedef _T = FoldableBlockType;
diff --git a/test/src/named_sections/parsers/brackets_start_end_test.dart b/test/src/named_sections/parsers/brackets_start_end_test.dart
index 58cb7dae..beb7967d 100644
--- a/test/src/named_sections/parsers/brackets_start_end_test.dart
+++ b/test/src/named_sections/parsers/brackets_start_end_test.dart
@@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
 
 void main() {
   test('BracketsStartEndNamedSectionParser. Parses comments', () {
-    const comments = [
+    final comments = [
       //
       SingleLineComment(lineIndex: 13, innerContent: ' no named section'),
 
diff --git a/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart b/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
index 99542c87..d24a8c67 100644
--- a/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
+++ b/test/src/single_line_comment/parser/highlight_single_line_comment_parser_test.dart
@@ -28,7 +28,7 @@ public class MyClass {
   text // slashed comment2 // still comment2
 }
 ''',
-        comments: const [
+        comments:[
           SingleLineComment(
             lineIndex: 0,
             characterIndex: 0,
@@ -56,7 +56,7 @@ public class MyClass { // comment
   private string str3 = "// not a comment";
 }
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 3,
             characterIndex: 49,
@@ -120,7 +120,7 @@ public class MyClass {
   text // slashed comment2 // still comment2
 }
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 0,
             characterIndex: 0,
@@ -152,7 +152,7 @@ public class MyClass { // comment
   #
 }
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 3,
             characterIndex: 49,
@@ -174,7 +174,7 @@ public class MyClass { // comment
 def fn():
   text # hash comment2 # still comment2
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 0,
             characterIndex: 0,
@@ -204,7 +204,7 @@ def fn(): # comment
   # not a comment
   """
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 0,
             characterIndex: 10,
@@ -238,7 +238,7 @@ public class MyClass {
   text // slashed comment2 // still comment2
 }
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 0,
             characterIndex: 0,
@@ -265,7 +265,7 @@ public class MyClass { // comment
   val str3 = "// not a comment";
 }
 ''',
-        comments: const [
+        comments: [
           SingleLineComment(
             lineIndex: 3,
             characterIndex: 49,
diff --git a/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart b/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
index a9e2ed8a..c8e41935 100644
--- a/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
+++ b/test/src/single_line_comment/parser/text_single_line_comment_parser_test.dart
@@ -17,7 +17,7 @@ text ~ tilde comment3
 'single // quotes ~ line'
 "double ~ quotes // line"
 /* multi //line comment */''';
-      const expected = [
+      final expected = [
         SingleLineComment(
           lineIndex: 0,
           characterIndex: 0,