Repository: akvelon/flutter-code-editor
Pull request #107 (2022-11-03 05:07:00)

issue 101 python block folding fix


Patch:
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index cc7461c..2df163a 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -169,9 +169,9 @@ class Code {
         continue;
       }
 
-      final lastLineIndex = section.lastLine ?? lines.length - 1;
+      final lastLineIndex = section.endLine ?? lines.length - 1;
 
-      for (int i = section.firstLine; i <= lastLineIndex; i++) {
+      for (int i = section.startLine; i <= lastLineIndex; i++) {
         lines[i] = lines[i].copyWith(isReadOnly: true);
       }
     }
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index ec9be44..8815790 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -2,7 +2,6 @@
 
 import 'dart:math';
 
-import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart' show kIsWeb;
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -499,46 +498,6 @@ class CodeController extends TextEditingController {
     );
   }
 
-  void foldCommentAtLineZero() {
-    final block = _code.foldableBlocks.firstOrNull;
-
-    if (block == null || !block.isComment || block.firstLine != 0) {
-      return;
-    }
-
-    foldAt(0);
-  }
-
-  void foldImports() {
-    // TODO(alexeyinkin): An optimized method to fold multiple blocks, https://github.com/akvelon/flutter-code-editor/issues/106
-    for (final block in _code.foldableBlocks) {
-      if (block.isImports) {
-        foldAt(block.firstLine);
-      }
-    }
-  }
-
-  /// Folds blocks that are outside all of the [names] sections.
-  ///
-  /// For a block to be not folded, it must overlap any of the given sections
-  /// in any way.
-  void foldOutsideSections(Iterable<String> names) {
-    final foldLines = {..._code.foldableBlocks.map((b) => b.firstLine)};
-    final sections = names.map((s) => _code.namedSections[s]).whereNotNull();
-
-    for (final block in _code.foldableBlocks) {
-      for (final section in sections) {
-        if (block.overlaps(section)) {
-          foldLines.remove(block.firstLine);
-          break;
-        }
-      }
-    }
-
-    // TODO(alexeyinkin): An optimized method to fold multiple blocks, https://github.com/akvelon/flutter-code-editor/issues/106
-    foldLines.forEach(foldAt);
-  }
-
   @override
   TextSpan buildTextSpan({
     required BuildContext context,
diff --git a/lib/src/folding/foldable_block.dart b/lib/src/folding/foldable_block.dart
index 2ac0cb5..7dc099f 100644
--- a/lib/src/folding/foldable_block.dart
+++ b/lib/src/folding/foldable_block.dart
@@ -2,20 +2,13 @@ import 'dart:math';
 
 import 'package:equatable/equatable.dart';
 
-import '../util/inclusive_range.dart';
 import 'foldable_block_type.dart';
 
-class FoldableBlock extends InclusiveRange with EquatableMixin {
+class FoldableBlock with EquatableMixin {
   final int firstLine;
   final int lastLine;
   final FoldableBlockType type;
 
-  @override
-  int get first => firstLine;
-
-  @override
-  int get last => lastLine;
-
   const FoldableBlock({
     required this.firstLine,
     required this.lastLine,
@@ -34,27 +27,6 @@ class FoldableBlock extends InclusiveRange with EquatableMixin {
   }
 
   int get lineCount => lastLine - firstLine + 1;
-
-  bool get isComment {
-    // ignore: missing_enum_constant_in_switch
-    switch (type) {
-      case FoldableBlockType.singleLineComment:
-      case FoldableBlockType.multilineComment:
-        return true;
-    }
-
-    return false;
-  }
-
-  bool get isImports {
-    // ignore: missing_enum_constant_in_switch
-    switch (type) {
-      case FoldableBlockType.imports:
-        return true;
-    }
-
-    return false;
-  }
 }
 
 extension FoldableBlockList on List<FoldableBlock> {
diff --git a/lib/src/folding/parsers/python.dart b/lib/src/folding/parsers/python.dart
index 083bad0..3136861 100644
--- a/lib/src/folding/parsers/python.dart
+++ b/lib/src/folding/parsers/python.dart
@@ -9,7 +9,7 @@ import 'indent.dart';
 ///A parser for foldable blocks for python
 class PythonFoldableBlockParser extends AbstractFoldableBlockParser {
   @override
-  List<FoldableBlock> parse({
+  void parse({
     required Result highlighted,
     required Set<Object?> serviceCommentsSources,
     required CodeLines lines,
@@ -27,14 +27,15 @@ class PythonFoldableBlockParser extends AbstractFoldableBlockParser {
       lines,
     );
 
-    if (indentBlocks.isEmpty) {
-      return highlightBlocks;
+    if (indentBlocks.isEmpty && highlightBlocks.isEmpty) {
+      return;
     }
 
-    return _combineBlocks(
+    final combinedBlocks = _combineBlocks(
       highlightBlocks: highlightBlocks,
       indentBlocks: indentBlocks,
     );
+    blocks.addAll(combinedBlocks);
   }
 
   List<FoldableBlock> _getBlocksFromParser(
diff --git a/lib/src/named_sections/named_section.dart b/lib/src/named_sections/named_section.dart
index df08757..35af363 100644
--- a/lib/src/named_sections/named_section.dart
+++ b/lib/src/named_sections/named_section.dart
@@ -1,36 +1,33 @@
-import 'package:equatable/equatable.dart';
+import 'package:meta/meta.dart';
 
-import '../util/inclusive_range.dart';
-
-class NamedSection extends InclusiveRange with EquatableMixin {
+@immutable
+class NamedSection {
   /// Zero-based index of the line with the starting tag.
-  final int firstLine;
+  final int startLine;
 
   /// Zero-based index of the line with the ending tag.
   /// `null` if the section spans till the end of the document.
-  final int? lastLine;
+  final int? endLine;
 
   final String name;
 
-  @override
-  int get first => firstLine;
-
-  @override
-  int? get last => lastLine;
-
   const NamedSection({
-    required this.firstLine,
-    required this.lastLine,
+    required this.startLine,
+    required this.endLine,
     required this.name,
   });
 
   @override
-  List<Object?> get props => [
-        firstLine,
-        lastLine,
-        name,
-      ];
+  int get hashCode => Object.hash(startLine, endLine, name);
+
+  @override
+  bool operator ==(Object other) {
+    return other is NamedSection &&
+        startLine == other.startLine &&
+        endLine == other.endLine &&
+        name == other.name;
+  }
 
   @override
-  String toString() => '$firstLine-$lastLine: "$name"';
+  String toString() => '$startLine-$endLine: "$name"';
 }
diff --git a/lib/src/named_sections/parsers/abstract.dart b/lib/src/named_sections/parsers/abstract.dart
index 1681add..58a5506 100644
--- a/lib/src/named_sections/parsers/abstract.dart
+++ b/lib/src/named_sections/parsers/abstract.dart
@@ -23,7 +23,7 @@ abstract class AbstractNamedSectionParser {
     );
 
     return unsorted.where(_isValid).toList(growable: false)
-      ..sort((a, b) => a.firstLine - b.firstLine);
+      ..sort((a, b) => a.startLine - b.startLine);
   }
 
   List<NamedSection> parseUnsorted({
@@ -31,8 +31,8 @@ abstract class AbstractNamedSectionParser {
   });
 
   bool _isValid(NamedSection section) {
-    final lastLine = section.lastLine;
-    return section.firstLine >= 0 &&
-        (lastLine == null || section.firstLine <= lastLine);
+    final endLine = section.endLine;
+    return section.startLine >= 0 &&
+        (endLine == null || section.startLine <= endLine);
   }
 }
diff --git a/lib/src/named_sections/parsers/brackets_start_end.dart b/lib/src/named_sections/parsers/brackets_start_end.dart
index 34efaea..5f4aa49 100644
--- a/lib/src/named_sections/parsers/brackets_start_end.dart
+++ b/lib/src/named_sections/parsers/brackets_start_end.dart
@@ -16,7 +16,7 @@ import 'abstract.dart';
 /// different comments.
 ///
 /// If a section is never started, it starts at the line 0.
-/// If a section is never ended, its `lastLine` is `null`.
+/// If a section is never ended, its `endLine` is `null`.
 ///
 /// If a section is started multiple times, the min line number takes effect.
 /// If a section is ended multiple times, the max line number takes effect.
@@ -32,56 +32,56 @@ class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
   List<NamedSection> parseUnsorted({
     required List<SingleLineComment> singleLineComments,
   }) {
-    final firsts = <String, int>{};
-    final lasts = <String, int>{};
+    final starts = <String, int>{};
+    final ends = <String, int>{};
 
     for (final comment in singleLineComments) {
       for (final match in _startRe.allMatches(comment.innerContent)) {
         final name = match.group(3) ?? '';
-        final oldFirst = firsts[name];
-        firsts[name] = oldFirst == null
+        final oldStart = starts[name];
+        starts[name] = oldStart == null
             ? comment.lineIndex
-            : min(comment.lineIndex, oldFirst);
+            : min(comment.lineIndex, oldStart);
       }
 
       for (final match in _endRe.allMatches(comment.innerContent)) {
         final name = match.group(3) ?? '';
-        lasts[name] = max(comment.lineIndex, lasts[name] ?? 0);
+        ends[name] = max(comment.lineIndex, ends[name] ?? 0);
       }
     }
 
-    return _combineFirstsAndLasts(firsts, lasts);
+    return _combineStartsAndEnds(starts, ends);
   }
 
-  List<NamedSection> _combineFirstsAndLasts(
-    Map<String, int> firsts,
-    Map<String, int> lasts,
+  List<NamedSection> _combineStartsAndEnds(
+    Map<String, int> starts,
+    Map<String, int> ends,
   ) {
     final sections = <NamedSection>[];
 
-    for (final entry in lasts.entries) {
+    for (final entry in ends.entries) {
       final name = entry.key;
-      final last = entry.value;
+      final end = entry.value;
 
       sections.add(
         NamedSection(
-          firstLine: firsts[name] ?? 0,
-          lastLine: last,
+          startLine: starts[name] ?? 0,
+          endLine: end,
           name: name,
         ),
       );
 
-      firsts.remove(name);
+      starts.remove(name);
     }
 
-    for (final entry in firsts.entries) {
+    for (final entry in starts.entries) {
       final name = entry.key;
-      final first = entry.value;
+      final start = entry.value;
 
       sections.add(
         NamedSection(
-          firstLine: first,
-          lastLine: lasts[name],
+          startLine: start,
+          endLine: ends[name],
           name: name,
         ),
       );
diff --git a/lib/src/util/inclusive_range.dart b/lib/src/util/inclusive_range.dart
deleted file mode 100644
index 7eff608..0000000
--- a/lib/src/util/inclusive_range.dart
+++ /dev/null
@@ -1,35 +0,0 @@
-/// A range with both [first] and [last] considered included.
-///
-/// This is different from a common 'start-end' model where the end
-/// is not included.
-abstract class InclusiveRange {
-  const InclusiveRange();
-
-  int get first;
-
-  /// The last included index, considered infinite if `null`.
-  int? get last;
-
-  bool overlaps(InclusiveRange other) {
-    final thisLast = last;
-    final otherLast = other.last;
-
-    if (thisLast == null) {
-      if (otherLast == null) {
-        return true;
-      }
-
-      return otherLast >= first;
-    }
-
-    if (otherLast == null) {
-      return thisLast >= other.first;
-    }
-
-    if (thisLast < other.first || otherLast < first) {
-      return false;
-    }
-
-    return true;
-  }
-}


Test patch:
diff --git a/test/src/code_field/code_controller_folding_editing_test.dart b/test/src/code_field/code_controller_folding_editing_test.dart
deleted file mode 100644
index 4429873..0000000
--- a/test/src/code_field/code_controller_folding_editing_test.dart
+++ /dev/null
@@ -1,291 +0,0 @@
-import 'package:flutter/widgets.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import '../common/create_app.dart';
-import '../common/snippets.dart';
-import '../common/text_editing_value.dart';
-import '../common/widget_tester.dart';
-
-const _commentsCode = '''
-private class MyClass {
-  //comment1
-  //comment2
-  void method() {}
-}
-''';
-
-void main() {
-  group('CodeController. Folding.', () {
-    group('Editing', () {
-      testWidgets('above a folded block', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
-        controller.foldAt(1);
-        await wt.selectFromHome(0, offset: 7);
-
-        controller.value = controller.value.replacedSelection('public');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-public class MyClass {
-  void method1() {
-
-  void method2() {
-    return;
-  }
-}
-''',
-            selection: TextSelection(baseOffset: 0, extentOffset: 6),
-          ),
-        );
-
-        controller.unfoldAt(1);
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-public class MyClass {
-  void method1() {
-    if (false) {
-      return;
-    }
-  }
-
-  void method2() {
-    return;
-  }
-}
-''',
-            selection: TextSelection(baseOffset: 0, extentOffset: 6),
-          ),
-        );
-      });
-
-      testWidgets('the first line of a folded block', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
-        controller.foldAt(0);
-        await wt.selectFromHome(0, offset: 7);
-
-        controller.value = controller.value.replacedSelection('public');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-public class MyClass {
-''',
-            selection: TextSelection(baseOffset: 0, extentOffset: 6),
-          ),
-        );
-
-        controller.unfoldAt(0);
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-public class MyClass {
-  void method1() {
-    if (false) {
-      return;
-    }
-  }
-
-  void method2() {
-    return;
-  }
-}
-''',
-            selection: TextSelection(baseOffset: 0, extentOffset: 6),
-          ),
-        );
-      });
-
-      testWidgets('between folded blocks', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
-        controller.foldAt(1);
-        controller.foldAt(7);
-
-        await wt.selectFromHome(43);
-        controller.value = controller.value.replacedSelection('int n;\n');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  void method1() {
-int n;
-
-  void method2() {
-}
-''',
-            selection: TextSelection.collapsed(offset: 50),
-          ),
-        );
-
-        controller.unfoldAt(1);
-        controller.unfoldAt(8);
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  void method1() {
-    if (false) {
-      return;
-    }
-  }
-int n;
-
-  void method2() {
-    return;
-  }
-}
-''',
-            selection: TextSelection.collapsed(offset: 91),
-          ),
-        );
-      });
-    });
-
-    group('Deleting folded blocks.', () {
-      testWidgets('First block of the same length', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
-        controller.foldAt(1);
-
-        await wt.selectFromHome(41, offset: 2);
-        // private class MyClass {\n  void method1() {
-        //                                           \ cursor
-        controller.value = controller.value.replacedSelection(';');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  void method1() ;
-  void method2() {
-    return;
-  }
-}
-''',
-            // TODO(alexeyinkin): Selection.
-            selection: TextSelection(baseOffset: 41, extentOffset: 42),
-          ),
-        );
-      });
-
-      testWidgets('Second block of the same length', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
-        controller.foldAt(7);
-        await wt.selectFromHome(102, offset: 2);
-        // ...void method2() {
-        //                   \ cursor
-
-        controller.value = controller.value.replacedSelection(';');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  void method1() {
-    if (false) {
-      return;
-    }
-  }
-
-  void method2() ;}
-''',
-            // TODO(alexeyinkin): Selection.
-            selection: TextSelection(baseOffset: 102, extentOffset: 103),
-          ),
-        );
-      });
-
-      // TODO(alexeyinkin): Fix, https://github.com/akvelon/flutter-code-editor/issues/83
-      testWidgets(
-        'When deleting 2nd identical folded block, 1st one incorrectly folds',
-        (WidgetTester wt) async {
-          final controller = await pumpController(wt, '''
-{
-if (true) {
-}
-if (true) {
-}
-}
-''');
-          controller.foldAt(3);
-          await wt.selectFromHome(26, offset: 2);
-          // {\nif (true) {\n}\nif (true) {}\n\n
-          //                              \ cursor
-
-          controller.value = controller.value.replacedSelection(';');
-
-          expect(
-            controller.value,
-            const TextEditingValue(
-              text: '''
-{
-if (true) {
-if (true) ;}
-''',
-              // TODO(alexeyinkin): Selection.
-              selection: TextSelection.collapsed(offset: 13),
-            ),
-          );
-        },
-      );
-
-      testWidgets('Deleting folded comments', (WidgetTester wt) async {
-        final controller = await pumpController(wt, _commentsCode);
-        controller.foldAt(1);
-        await wt.selectFromHome(26, offset: 13);
-        // private class MyClass {\n  //comment1\n  void method...
-        //                            \--selected-->
-
-        controller.value = controller.value.replacedSelection('');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  void method() {}
-}
-''',
-            selection: TextSelection.collapsed(offset: 26),
-          ),
-        );
-      });
-
-      testWidgets('Inserting after folded comments', (WidgetTester wt) async {
-        final controller = await pumpController(wt, _commentsCode);
-        controller.foldAt(1);
-        await wt.selectFromHome(37);
-        // private class MyClass {\n  //comment1\n  void method...
-        //                                        \ cursor
-
-        controller.value = controller.value.replacedSelection('  int n;\n');
-
-        expect(
-          controller.value,
-          const TextEditingValue(
-            text: '''
-private class MyClass {
-  //comment1
-  int n;
-  void method() {}
-}
-''',
-            selection: TextSelection.collapsed(offset: 46),
-          ),
-        );
-      });
-    });
-  });
-}
diff --git a/test/src/code_field/code_controller_folding_test.dart b/test/src/code_field/code_controller_folding_test.dart
index 5057ab5..b43e639 100644
--- a/test/src/code_field/code_controller_folding_test.dart
+++ b/test/src/code_field/code_controller_folding_test.dart
@@ -1,12 +1,24 @@
-// ignore_for_file: prefer_interpolation_to_compose_strings
-
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 import '../common/create_app.dart';
-import '../common/snippets.dart';
+import '../common/text_editing_value.dart';
 import '../common/widget_tester.dart';
 
+const _code = '''
+private class MyClass {
+  void method1() {
+    if (false) {// [START section1]
+      return;
+    }// [END section1]
+  }
+
+  void method2() {// [START section2]
+    return;
+  }// [END section2]
+}
+''';
+
 const _codeFolded1 = '''
 private class MyClass {
   void method1() {
@@ -17,6 +29,14 @@ private class MyClass {
 }
 ''';
 
+const _commentsCode = '''
+private class MyClass {
+  //comment1
+  //comment2
+  void method() {}
+}
+''';
+
 void main() {
   setUp(() {
     focusNode = FocusNode();
@@ -39,7 +59,7 @@ int c;
       });
 
       test('Folding does nothing if no foldable block on the line', () {
-        final controller = createController(TwoMethodsSnippet.full);
+        final controller = createController(_code);
         final oldCode = controller.code;
 
         controller.foldAt(3);
@@ -48,7 +68,7 @@ int c;
       });
 
       test('Double folding changes nothing', () {
-        final controller = createController(TwoMethodsSnippet.full);
+        final controller = createController(_code);
         controller.foldAt(1);
         final oldCode = controller.code;
 
@@ -58,7 +78,7 @@ int c;
       });
 
       test('Unfolding does nothing if no foldable block on the line', () {
-        final controller = createController(TwoMethodsSnippet.full);
+        final controller = createController(_code);
         final oldCode = controller.code;
 
         controller.unfoldAt(3);
@@ -67,7 +87,7 @@ int c;
       });
 
       test('Unfolding non-folded changes nothing', () {
-        final controller = createController(TwoMethodsSnippet.full);
+        final controller = createController(_code);
         final oldCode = controller.code;
 
         controller.unfoldAt(1);
@@ -78,7 +98,7 @@ int c;
 
     group('Hides text.', () {
       test('Hides highlighted', () {
-        final controller = createController(TwoMethodsSnippet.full);
+        final controller = createController(_code);
         final originalHtml = controller.code.visibleHighlighted?.toHtml();
 
         controller.foldAt(1);
@@ -102,7 +122,7 @@ int c;
       });
 
       testWidgets('Cursor before', (WidgetTester wt) async {
-        final controller = await pumpController(wt, TwoMethodsSnippet.full);
+        final controller = await pumpController(wt, _code);
         final textBefore = controller.rawText;
         await wt.selectFromHome(1, offset: 1);
 
@@ -128,243 +148,274 @@ int c;
       });
     });
 
-    group('foldCommentAtLineZero', () {
-      test('folds single line comments at line 0', () {
-        final controller = createController(CommentImportSnippet.full);
+    group('Editing', () {
+      testWidgets('above a folded block', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _code);
+        controller.foldAt(1);
+        await wt.selectFromHome(0, offset: 7);
 
-        controller.foldCommentAtLineZero();
+        controller.value = controller.value.replacedSelection('public');
 
         expect(
-          controller.text,
-          '''
-// comment1
+          controller.value,
+          const TextEditingValue(
+            text: '''
+public class MyClass {
+  void method1() {
+
+  void method2() {
+    return;
+  }
+}
+''',
+            selection: TextSelection(baseOffset: 0, extentOffset: 6),
+          ),
+        );
 
-package mypackage;
-import java.util.Arrays;
+        controller.unfoldAt(1);
 
-{
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '''
+public class MyClass {
+  void method1() {
+    if (false) {
+      return;
+    }
+  }
+
+  void method2() {
+    return;
+  }
 }
 ''',
+            selection: TextSelection(baseOffset: 0, extentOffset: 6),
+          ),
         );
       });
 
-      test('folds multiline comments at line 0', () {
-        final controller = createController(
-          '/*\n*/\n' + CommentImportSnippet.full,
+      testWidgets('the first line of a folded block', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _code);
+        controller.foldAt(0);
+        await wt.selectFromHome(0, offset: 7);
+
+        controller.value = controller.value.replacedSelection('public');
+
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '''
+public class MyClass {
+''',
+            selection: TextSelection(baseOffset: 0, extentOffset: 6),
+          ),
         );
 
-        controller.foldCommentAtLineZero();
+        controller.unfoldAt(0);
 
         expect(
-          controller.text,
-          '/*\n' + CommentImportSnippet.visible,
+          controller.value,
+          const TextEditingValue(
+            text: '''
+public class MyClass {
+  void method1() {
+    if (false) {
+      return;
+    }
+  }
+
+  void method2() {
+    return;
+  }
+}
+''',
+            selection: TextSelection(baseOffset: 0, extentOffset: 6),
+          ),
         );
       });
 
-      test('does not fold if the comment is not on line 0', () {
-        const prefixes = [
-          '\n',
-          '{\n}\n',
-        ];
-
-        for (final prefix in prefixes) {
-          final controller = createController(
-            prefix + CommentImportSnippet.full,
-          );
+      testWidgets('between folded blocks', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _code);
+        controller.foldAt(1);
+        controller.foldAt(7);
 
-          controller.foldCommentAtLineZero();
+        await wt.selectFromHome(43);
+        controller.value = controller.value.replacedSelection('int n;\n');
 
-          expect(
-            controller.text,
-            prefix + CommentImportSnippet.visible,
-            reason: prefix,
-          );
-        }
-      });
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  void method1() {
+int n;
 
-      test('does nothing if no foldable comment blocks', () {
-        const texts = [
-          '',
-          'int n;\nint m;',
-          '{\n}\n',
-        ];
+  void method2() {
+}
+''',
+            selection: TextSelection.collapsed(offset: 50),
+          ),
+        );
 
-        for (final text in texts) {
-          final controller = createController(text);
+        controller.unfoldAt(1);
+        controller.unfoldAt(8);
 
-          controller.foldCommentAtLineZero();
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  void method1() {
+    if (false) {
+      return;
+    }
+  }
+int n;
 
-          expect(controller.text, text, reason: text);
-        }
+  void method2() {
+    return;
+  }
+}
+''',
+            selection: TextSelection.collapsed(offset: 91),
+          ),
+        );
       });
     });
 
-    group('foldImports', () {
-      test('folds single line comments at line 0', () {
-        final controller = createController(CommentImportSnippet.full);
+    group('Deleting folded blocks.', () {
+      testWidgets('First block of the same length', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _code);
+        controller.foldAt(1);
 
-        controller.foldImports();
+        await wt.selectFromHome(41, offset: 2);
+        // private class MyClass {\n  void method1() {
+        //                                           \ cursor
+        controller.value = controller.value.replacedSelection(';');
 
         expect(
-          controller.text,
-          '''
-// comment1
-///comment2
-
-package mypackage;
-
-{
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  void method1() ;
+  void method2() {
+    return;
+  }
 }
 ''',
+            // TODO(alexeyinkin): Selection.
+            selection: TextSelection(baseOffset: 41, extentOffset: 42),
+          ),
         );
       });
 
-      test('does nothing if no import blocks', () {
-        const texts = [
-          '',
-          'int n;\nint m;',
-          '{\n}\n',
-          '//\n//\n',
-        ];
-
-        for (final text in texts) {
-          final controller = createController(text);
+      testWidgets('Second block of the same length', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _code);
+        controller.foldAt(7);
+        await wt.selectFromHome(102, offset: 2);
+        // ...void method2() {
+        //                   \ cursor
 
-          controller.foldImports();
+        controller.value = controller.value.replacedSelection(';');
 
-          expect(controller.text, text, reason: text);
-        }
-      });
-    });
+        expect(
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  void method1() {
+    if (false) {
+      return;
+    }
+  }
 
-    group('foldOutsideSections.', () {
-      test('No blocks -> Do nothing', () {
-        const texts = [
-          '',
-          'int n;',
-        ];
-
-        const sectionLists = <List<String>>[
-          [],
-          ['nonexistent'],
-        ];
-
-        for (final text in texts) {
-          for (final sections in sectionLists) {
-            final controller = createController(text);
-
-            controller.foldOutsideSections(sections);
-
-            expect(
-              controller.code.visibleText,
-              text,
-              reason: '$text $sections',
-            );
-          }
-        }
+  void method2() ;}
+''',
+            // TODO(alexeyinkin): Selection.
+            selection: TextSelection(baseOffset: 102, extentOffset: 103),
+          ),
+        );
       });
 
-      test('No sections -> Fold everything', () {
-        const text = '''
-{{
+      // TODO(alexeyinkin): Fix, https://github.com/akvelon/flutter-code-editor/issues/83
+      testWidgets(
+        'When deleting 2nd identical folded block, 1st one incorrectly folds',
+        (WidgetTester wt) async {
+          final controller = await pumpController(wt, '''
+{
+if (true) {
 }
+if (true) {
 }
-{
 }
-''';
-        const expected = '''
-{{
+''');
+          controller.foldAt(3);
+          await wt.selectFromHome(26, offset: 2);
+          // {\nif (true) {\n}\nif (true) {}\n\n
+          //                              \ cursor
+
+          controller.value = controller.value.replacedSelection(';');
+
+          expect(
+            controller.value,
+            const TextEditingValue(
+              text: '''
 {
-''';
-        final controller = createController(text);
+if (true) {
+if (true) ;}
+''',
+              // TODO(alexeyinkin): Selection.
+              selection: TextSelection.collapsed(offset: 13),
+            ),
+          );
+        },
+      );
 
-        controller.foldOutsideSections([]);
+      testWidgets('Deleting folded comments', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _commentsCode);
+        controller.foldAt(1);
+        await wt.selectFromHome(26, offset: 13);
+        // private class MyClass {\n  //comment1\n  void method...
+        //                            \--selected-->
+
+        controller.value = controller.value.replacedSelection('');
 
         expect(
-          controller.code.visibleText,
-          expected,
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  void method() {}
+}
+''',
+            selection: TextSelection.collapsed(offset: 26),
+          ),
         );
       });
 
-      test('Folds specific sections', () {
-        const text = '''
-{                                     //  0
-  {                                   //  1
-  }                                   //  2
-                                      //  3
-  {//[START matches_block]                4
-  }//[END matches_block]                  5
-                                      //  6
-  {//[START matches_block_fold]           7
-  }//[END matches_block_fold]             8
-                                      //  9
-  {                                   //  10
-//[START outlives_block]                  11
-  }                                   //  12
-//[END outlives_block]                    13
-                                      //  14
-//[START outlived_by_block]               15
-  {                                   //  16
-//[END outlived_by_block]                 17
-  }                                   //  18
-                                      //  19
-//[START contains_blocks]                 20
-  {                                   //  21
-  }                                   //  22
-                                      //  23
-  {                                   //  24
-  }                                   //  25
-//[END contains_blocks]                   26
-                                      //  27
-  {                                   //  28
-  }                                   //  29
-}                                     //  30
-''';
-
-        const foldedVisible = '''
-{                                     //  0
-  {                                   //  1
-                                      //  3
-  {
-  }
-                                      //  6
-  {
-                                      //  9
-  {                                   //  10
-
-  }                                   //  12
-
-                                      //  14
-
-  {                                   //  16
-
-  }                                   //  18
-                                      //  19
-
-  {                                   //  21
-  }                                   //  22
-                                      //  23
-  {                                   //  24
-  }                                   //  25
-
-                                      //  27
-  {                                   //  28
-}                                     //  30
-''';
-
-        final controller = createController(text);
+      testWidgets('Inserting after folded comments', (WidgetTester wt) async {
+        final controller = await pumpController(wt, _commentsCode);
+        controller.foldAt(1);
+        await wt.selectFromHome(37);
+        // private class MyClass {\n  //comment1\n  void method...
+        //                                        \ cursor
 
-        controller.foldOutsideSections([
-          'matches_block',
-          'outlives_block',
-          'outlived_by_block',
-          'contains_blocks',
-          'nonexistent',
-        ]);
+        controller.value = controller.value.replacedSelection('  int n;\n');
 
         expect(
-          controller.code.visibleText,
-          foldedVisible,
+          controller.value,
+          const TextEditingValue(
+            text: '''
+private class MyClass {
+  //comment1
+  int n;
+  void method() {}
+}
+''',
+            selection: TextSelection.collapsed(offset: 46),
+          ),
         );
       });
     });
diff --git a/test/src/code_field/code_controller_shortcut_test.dart b/test/src/code_field/code_controller_shortcut_test.dart
index 3d63c24..2af07b2 100644
--- a/test/src/code_field/code_controller_shortcut_test.dart
+++ b/test/src/code_field/code_controller_shortcut_test.dart
@@ -101,10 +101,10 @@ void main() {
 
         await example.act();
 
-        expect(calls.length, 1, reason: example.name);
-        expect(calls[0].method, 'Clipboard.setData', reason: example.name);
-        expect(calls[0].arguments, {'text': _copiedText}, reason: example.name);
-        expect(controller.text, example.visibleTextAfter, reason: example.name);
+        expect(calls.length, 1);
+        expect(calls[0].method, 'Clipboard.setData');
+        expect(calls[0].arguments, {'text': _copiedText});
+        expect(controller.text, example.visibleTextAfter);
       }
     });
   });
diff --git a/test/src/common/snippets.dart b/test/src/common/snippets.dart
index f47f014..d95f435 100644
--- a/test/src/common/snippets.dart
+++ b/test/src/common/snippets.dart
@@ -19,34 +19,3 @@ class MyClass {
 }
 ''';
 }
-
-class TwoMethodsSnippet {
-  static const full = '''
-private class MyClass {
-  void method1() {
-    if (false) {// [START section1]
-      return;
-    }// [END section1]
-  }
-
-  void method2() {// [START section2]
-    return;
-  }// [END section2]
-}
-''';
-}
-
-class CommentImportSnippet {
-  static const full = '''
-// comment1
-///comment2
-
-package mypackage;
-import java.util.Arrays;
-
-{
-}
-''';
-
-  static String get visible => full;
-}
diff --git a/test/src/folding/parsers/python_parser_test.dart b/test/src/folding/parsers/python_parser_test.dart
index 1ba760c..d74733f 100644
--- a/test/src/folding/parsers/python_parser_test.dart
+++ b/test/src/folding/parsers/python_parser_test.dart
@@ -265,14 +265,16 @@ a = [[
           readonlyCommentsByLine: commentParser.getIfReadonlyCommentByLine(),
         );
 
-        final pythonParser = PythonFoldableBlockParser().parse(
-          highlighted: highlighted,
-          serviceCommentsSources: serviceComments.map((e) => e.source).toSet(),
-          lines: codeLines,
-        );
+        final pythonParser = PythonFoldableBlockParser()
+          ..parse(
+            highlighted: highlighted,
+            serviceCommentsSources:
+                serviceComments.map((e) => e.source).toSet(),
+            lines: codeLines,
+          );
 
         expect(
-          pythonParser,
+          pythonParser.blocks,
           example.expected,
           reason: '${example.name}, valid blocks',
         );
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 a86ff62..beb7967 100644
--- a/test/src/named_sections/parsers/brackets_start_end_test.dart
+++ b/test/src/named_sections/parsers/brackets_start_end_test.dart
@@ -68,23 +68,23 @@ void main() {
     ];
 
     const expected = [
-      NamedSection(firstLine: 0, lastLine: 14, name: 'never_started'),
-      NamedSection(firstLine: 11, lastLine: null, name: 'never_ending'),
-      NamedSection(firstLine: 16, lastLine: 21, name: 'outer'),
-      NamedSection(firstLine: 18, lastLine: 21, name: 'inner'),
-      NamedSection(firstLine: 24, lastLine: 35, name: 'overlapping1'),
-      NamedSection(firstLine: 30, lastLine: 40, name: 'overlapping2'),
-      NamedSection(firstLine: 41, lastLine: 45, name: 'duplicate_start'),
-      NamedSection(firstLine: 61, lastLine: 67, name: 'duplicate_end'),
-      NamedSection(firstLine: 71, lastLine: 72, name: 'text_before'),
-      NamedSection(firstLine: 72, lastLine: 73, name: 'START'),
-      NamedSection(firstLine: 73, lastLine: 74, name: 'text_after'),
-      NamedSection(firstLine: 74, lastLine: 75, name: 'END'),
-      NamedSection(firstLine: 80, lastLine: 80, name: 'line'),
-      NamedSection(firstLine: 81, lastLine: 81, name: 'swap'),
-      NamedSection(firstLine: 82, lastLine: 82, name: 'split_line'),
-      NamedSection(firstLine: 83, lastLine: null, name: '2'),
-      NamedSection(firstLine: 84, lastLine: null, name: '_'),
+      NamedSection(startLine: 0, endLine: 14, name: 'never_started'),
+      NamedSection(startLine: 11, endLine: null, name: 'never_ending'),
+      NamedSection(startLine: 16, endLine: 21, name: 'outer'),
+      NamedSection(startLine: 18, endLine: 21, name: 'inner'),
+      NamedSection(startLine: 24, endLine: 35, name: 'overlapping1'),
+      NamedSection(startLine: 30, endLine: 40, name: 'overlapping2'),
+      NamedSection(startLine: 41, endLine: 45, name: 'duplicate_start'),
+      NamedSection(startLine: 61, endLine: 67, name: 'duplicate_end'),
+      NamedSection(startLine: 71, endLine: 72, name: 'text_before'),
+      NamedSection(startLine: 72, endLine: 73, name: 'START'),
+      NamedSection(startLine: 73, endLine: 74, name: 'text_after'),
+      NamedSection(startLine: 74, endLine: 75, name: 'END'),
+      NamedSection(startLine: 80, endLine: 80, name: 'line'),
+      NamedSection(startLine: 81, endLine: 81, name: 'swap'),
+      NamedSection(startLine: 82, endLine: 82, name: 'split_line'),
+      NamedSection(startLine: 83, endLine: null, name: '2'),
+      NamedSection(startLine: 84, endLine: null, name: '_'),
     ];
 
     const parser = BracketsStartEndNamedSectionParser();
diff --git a/test/src/util/inclusive_range_test.dart b/test/src/util/inclusive_range_test.dart
deleted file mode 100644
index 684eaf9..0000000
--- a/test/src/util/inclusive_range_test.dart
+++ /dev/null
@@ -1,90 +0,0 @@
-import 'package:flutter_code_editor/src/util/inclusive_range.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-void main() {
-  group('InclusiveRange', () {
-    test('overlaps', () {
-      const examples = [
-        //
-        _OverlapsExample(
-          'Finite. Finite. Overlap',
-          first: _InclusiveRange(1, 5),
-          second: _InclusiveRange(5, 10),
-          expected: true,
-        ),
-
-        _OverlapsExample(
-          'Finite. Finite. Separate',
-          first: _InclusiveRange(1, 5),
-          second: _InclusiveRange(6, 10),
-          expected: false,
-        ),
-
-        _OverlapsExample(
-          'Finite. Finite. Nested',
-          first: _InclusiveRange(6, 9),
-          second: _InclusiveRange(1, 10),
-          expected: true,
-        ),
-
-        _OverlapsExample(
-          'Finite. Infinite. Overlap',
-          first: _InclusiveRange(1, 5),
-          second: _InclusiveRange(5, null),
-          expected: true,
-        ),
-
-        _OverlapsExample(
-          'Finite. Infinite. Separate',
-          first: _InclusiveRange(1, 5),
-          second: _InclusiveRange(6, null),
-          expected: false,
-        ),
-
-        _OverlapsExample(
-          'Finite. Infinite. Nested',
-          first: _InclusiveRange(6, 9),
-          second: _InclusiveRange(1, null),
-          expected: true,
-        ),
-      ];
-
-      for (final example in examples) {
-        expect(
-          example.first.overlaps(example.second),
-          example.expected,
-          reason: example.name,
-        );
-        expect(
-          example.second.overlaps(example.first),
-          example.expected,
-          reason: '${example.name} inverted',
-        );
-      }
-    });
-  });
-}
-
-class _InclusiveRange extends InclusiveRange {
-  @override
-  final int first;
-
-  @override
-  final int? last;
-
-  const _InclusiveRange(this.first, this.last);
-}
-
-class _OverlapsExample {
-  final String name;
-  final _InclusiveRange first;
-  final _InclusiveRange second;
-  final bool expected;
-
-  const _OverlapsExample(
-    this.name, {
-    required this.first,
-    required this.second,
-    required this.expected,
-  });
-}