Repository: akvelon/flutter-code-editor
Pull request #119 (2022-11-09 12:42:15)

Issue103 pale readonly sections


Patch:
diff --git a/example/lib/custom_code_box.dart b/example/lib/custom_code_box.dart
index 4e71460..65b3f11 100644
--- a/example/lib/custom_code_box.dart
+++ b/example/lib/custom_code_box.dart
@@ -2,7 +2,6 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
-import 'package:flutter_code_editor/src/wip/autoRefactorService.dart';
 import 'package:highlight/languages/dart.dart';
 import 'package:highlight/languages/go.dart';
 import 'package:highlight/languages/java.dart';
@@ -339,19 +338,6 @@ private class MyClass {
                 ),
               ),
             ),
-            Align(
-              alignment: Alignment.topRight,
-              child: FloatingActionButton(
-                backgroundColor: Colors.indigo[800],
-                onPressed: () {
-                  setState(() {
-                    _codeController!.text =
-                        autoRefactor(_codeController!.text, widget.language);
-                  });
-                },
-                child: const Icon(Icons.format_align_left_outlined),
-              ),
-            ),
           ],
         ),
       ),
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index 98f2846..5b35fea 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:highlight/highlight_core.dart';
 
+import '../../src/highlight/result.dart';
 import '../folding/foldable_block.dart';
 import '../folding/foldable_block_matcher.dart';
 import '../folding/parsers/parser_factory.dart';
@@ -141,7 +142,8 @@ class Code {
       issues: issues,
       lines: lines,
       namedSections: sectionsMap,
-      visibleHighlighted: hiddenRanges.cutHighlighted(highlighted),
+      visibleHighlighted:
+          hiddenRanges.cutHighlighted(highlighted)?.splitLines(),
       visibleText: hiddenRanges.cutString(text),
       visibleSectionNames: visibleSectionNames,
     );
@@ -489,7 +491,7 @@ class Code {
       issues: issues,
       lines: lines,
       namedSections: namedSections,
-      visibleHighlighted: hiddenRanges.cutHighlighted(highlighted),
+      visibleHighlighted: hiddenRanges.cutHighlighted(highlighted)?.splitLines(),
       visibleText: hiddenRanges.cutString(text),
       visibleSectionNames: visibleSectionNames,
     );
diff --git a/lib/src/code/code_line.dart b/lib/src/code/code_line.dart
index 8884f82..f85fd07 100644
--- a/lib/src/code/code_line.dart
+++ b/lib/src/code/code_line.dart
@@ -9,31 +9,43 @@ class CodeLine {
 
   final TextRange textRange;
   final bool isReadOnly;
-  late final indent = _calculateIndent();
+  final int indent;
 
-  CodeLine({
+  const CodeLine({
     required this.text,
     required this.textRange,
+    required this.indent,
     this.isReadOnly = false,
   });
 
+  CodeLine.fromTextAndRange({
+    required this.text,
+    required this.textRange,
+    this.isReadOnly = false,
+  }) : indent = _calculateIndent(text);
+
   CodeLine.fromTextAndStart(
     this.text,
     int start, {
     this.isReadOnly = false,
-  }) : textRange = TextRange(start: start, end: start + text.length);
+  })  : textRange = TextRange(start: start, end: start + text.length),
+        indent = _calculateIndent(text);
 
   @override
   String toString() =>
       'CodeLine(ro: $isReadOnly, textRange: $textRange, text: "$text")';
 
-  CodeLine copyWith({bool? isReadOnly}) {
-    return CodeLine(
-      text: text,
-      textRange: textRange,
-      isReadOnly: isReadOnly ?? this.isReadOnly,
-    );
-  }
+  CodeLine copyWith({
+    String? text,
+    TextRange? textRange,
+    bool? isReadOnly,
+  }) =>
+      CodeLine(
+        text: text ?? this.text,
+        textRange: textRange ?? this.textRange,
+        isReadOnly: isReadOnly ?? this.isReadOnly,
+        indent: text == null ? 0 : _calculateIndent(text),
+      );
 
   @override
   bool operator ==(Object other) {
@@ -50,7 +62,7 @@ class CodeLine {
         isReadOnly,
       );
 
-  int _calculateIndent() {
+  static int _calculateIndent(String text) {
     int indentation = 0;
     for (final character in text.runes) {
       if (character == $space || character == $tab || character == $lf) {
diff --git a/lib/src/code/code_lines.dart b/lib/src/code/code_lines.dart
index cb569aa..1a4a2d6 100644
--- a/lib/src/code/code_lines.dart
+++ b/lib/src/code/code_lines.dart
@@ -1,3 +1,5 @@
+import 'dart:ui';
+
 import 'package:characters/characters.dart';
 import 'package:collection/collection.dart';
 import 'package:equatable/equatable.dart';
@@ -11,7 +13,17 @@ class CodeLines with EquatableMixin {
 
   const CodeLines(this.lines);
 
-  static const empty = CodeLines([]);
+  static const empty = CodeLines([
+    CodeLine(
+      text: '',
+      textRange: TextRange.collapsed(0),
+      indent: 0,
+    ),
+  ]);
+
+  CodeLine operator [](int i) => lines[i];
+
+  int get length => lines.length;
 
   /// Returns the 0-based line number of the character at [characterIndex].
   ///
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index 501aaac..0da5c1c 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -428,38 +428,6 @@ class CodeController extends TextEditingController {
     return text;
   }
 
-  TextSpan _processPatterns(String text, TextStyle? style) {
-    final children = <TextSpan>[];
-
-    text.splitMapJoin(
-      styleRegExp!,
-      onMatch: (Match m) {
-        if (styleList.isEmpty) {
-          return '';
-        }
-
-        int idx;
-        for (idx = 1;
-            idx < m.groupCount &&
-                idx <= styleList.length &&
-                m.group(idx) == null;
-            idx++) {}
-
-        children.add(TextSpan(
-          text: m[0],
-          style: styleList[idx - 1],
-        ));
-        return '';
-      },
-      onNonMatch: (String span) {
-        children.add(TextSpan(text: span, style: style));
-        return '';
-      },
-    );
-
-    return TextSpan(style: style, children: children);
-  }
-
   Future<void> generateSuggestions() async {
     final prefix = value.wordToCursor;
     if (prefix == null) {
@@ -587,11 +555,11 @@ class CodeController extends TextEditingController {
 
     // Return parsing
     if (_language != null) {
-      return const SpanBuilder().build(
+      return SpanBuilder(
         code: _code,
         theme: _getTheme(context),
         textStyle: style,
-      );
+      ).build();
     }
 
     if (styleRegExp != null) {
@@ -601,6 +569,38 @@ class CodeController extends TextEditingController {
     return TextSpan(text: text, style: style);
   }
 
+  TextSpan _processPatterns(String text, TextStyle? style) {
+    final children = <TextSpan>[];
+
+    text.splitMapJoin(
+      styleRegExp!,
+      onMatch: (Match m) {
+        if (styleList.isEmpty) {
+          return '';
+        }
+
+        int idx;
+        for (idx = 1;
+            idx < m.groupCount &&
+                idx <= styleList.length &&
+                m.group(idx) == null;
+            idx++) {}
+
+        children.add(TextSpan(
+          text: m[0],
+          style: styleList[idx - 1],
+        ));
+        return '';
+      },
+      onNonMatch: (String span) {
+        children.add(TextSpan(text: span, style: style));
+        return '';
+      },
+    );
+
+    return TextSpan(style: style, children: children);
+  }
+
   CodeThemeData _getTheme(BuildContext context) {
     return CodeTheme.of(context) ?? CodeThemeData(styles: _theme ?? {});
   }
diff --git a/lib/src/code_field/span_builder.dart b/lib/src/code_field/span_builder.dart
index a7b56da..cc3533c 100644
--- a/lib/src/code_field/span_builder.dart
+++ b/lib/src/code_field/span_builder.dart
@@ -1,17 +1,25 @@
 import 'package:flutter/widgets.dart';
 import 'package:highlight/highlight_core.dart';
 
+import '../../src/highlight/node.dart';
 import '../code/code.dart';
 import '../code_theme/code_theme_data.dart';
 
 class SpanBuilder {
-  const SpanBuilder();
+  final Code code;
+  final CodeThemeData? theme;
+  final TextStyle? textStyle;
 
-  TextSpan build({
-    required Code code,
-    required CodeThemeData? theme,
-    TextStyle? textStyle,
-  }) {
+  int _visibleLineIndex = 0;
+
+  SpanBuilder({
+    required this.code,
+    required this.theme,
+    this.textStyle,
+  });
+
+  TextSpan build() {
+    _visibleLineIndex = 0;
     return TextSpan(
       style: textStyle,
       children: _buildList(
@@ -38,10 +46,53 @@ class SpanBuilder {
     required Node node,
     required CodeThemeData? theme,
   }) {
+    _updateLineIndex(node);
+
+    final style = theme?.styles[node.className];
+
     return TextSpan(
       text: node.value,
-      children: _buildList(nodes: node.children, theme: theme),
-      style: theme?.styles[node.className],
+      children: _buildList(
+        nodes: node.children,
+        theme: theme,
+      ),
+      style: _paleIfRequired(style),
+    );
+  }
+
+  void _updateLineIndex(Node node) {
+    _visibleLineIndex += node.getNewlineCount();
+
+    if (_visibleLineIndex >= code.lines.length) {
+      _visibleLineIndex = code.lines.length - 1;
+    }
+  }
+
+  TextStyle? _paleIfRequired(TextStyle? style) {
+    final fullLineIndex =
+        code.hiddenLineRanges.recoverLineIndex(_visibleLineIndex);
+    if (code.lines[fullLineIndex].isReadOnly) {
+      return (style ?? textStyle)?.paled();
+    }
+    return style;
+  }
+}
+
+extension TextStyleExtension on TextStyle {
+  TextStyle paled() {
+    final clr = color;
+
+    if (clr == null) {
+      return this;
+    }
+
+    return copyWith(
+      color: Color.fromARGB(
+        clr.alpha ~/ 2,
+        clr.red,
+        clr.green,
+        clr.blue,
+      ),
     );
   }
 }
diff --git a/lib/src/hidden_ranges/hidden_line_ranges.dart b/lib/src/hidden_ranges/hidden_line_ranges.dart
index 3836151..3305b87 100644
--- a/lib/src/hidden_ranges/hidden_line_ranges.dart
+++ b/lib/src/hidden_ranges/hidden_line_ranges.dart
@@ -6,102 +6,72 @@ class HiddenLineRanges with EquatableMixin {
   final List<LineNumberingBreakpoint> breakpoints;
   final int fullLineCount;
 
-  const HiddenLineRanges({
-    required this.breakpoints,
-    required this.fullLineCount,
-  });
+  final List<int?> _fullToVisible;
+  final List<int> _visibleToFull;
 
-  static const empty = HiddenLineRanges(
-    breakpoints: [],
-    fullLineCount: 1,
-  );
+  factory HiddenLineRanges({
+    required List<LineNumberingBreakpoint> breakpoints,
+    required int fullLineCount,
+  }) {
+    final fullToVisible = List<int?>.filled(fullLineCount, null);
+    final visibleToFull = List<int>.filled(fullLineCount, 0);
 
-  /// Returns the visible line index to which the full [lineIndex] maps
-  /// and null if the line is hidden.
-  ///
-  /// Without breakpoints, returns [lineIndex] unchanged.
-  /// At a breakpoint, returns its `visibleLineIndex`.
-  /// Otherwise finds a next or previous breakpoint
-  /// Before the first breakpoint, returns [lineIndex] unchanged.
-  /// Otherwise finds the first preceding breakpoint and uses subtracts
-  /// its spread from [lineIndex].
-  ///
-  /// [lineIndex] can be any integer including negative or >= [fullLineCount].
-  int? cutLineIndexIfVisible(int lineIndex) {
-    if (breakpoints.isEmpty) {
-      return lineIndex;
-    }
-    if (lineIndex <= breakpoints.first.full) {
-      return breakpoints.first.cutLineIndexIfVisible(lineIndex);
-    }
-    if (lineIndex >= breakpoints.last.full) {
-      return breakpoints.last.cutLineIndexIfVisible(lineIndex);
-    }
+    var fullIndex = 0;
+    var visibleIndex = 0;
 
-    // At this point, we always have a breakpoint before and after.
-
-    int lower = 0;
-    int upper = breakpoints.length - 1;
-
-    // Linear interpolation search.
-    while (upper > lower) {
-      final lowerLineIndex = breakpoints[lower].full;
-      final upperLineIndex = breakpoints[upper].full;
-
-      final index = (lower +
-              (lineIndex - lowerLineIndex) /
-                  (upperLineIndex - lowerLineIndex) *
-                  (upper - lower))
-          .floor();
-
-      final breakpoint = breakpoints[index];
-
-      switch ((breakpoint.full - lineIndex).sign) {
-        case -1: // We are after this breakpoint.
-          final nextBreakpoint = breakpoints[index + 1];
-          if (nextBreakpoint.full > lineIndex) {
-            // ... and before the next one.
-            return nextBreakpoint.cutLineIndexIfVisible(lineIndex);
-          }
-          lower = index + 1;
-          continue;
-
-        case 1: // We are before this breakpoint.
-          final previousBreakpoint = breakpoints[index - 1];
-          if (previousBreakpoint.full < lineIndex) {
-            // ... and after the previous one.
-            return breakpoint.cutLineIndexIfVisible(lineIndex);
-          }
-          upper = index - 1;
-          continue;
+    for (final breakpoint in breakpoints) {
+      final to = breakpoint.fullBefore;
+
+      while (fullIndex < to) {
+        visibleToFull[visibleIndex] = fullIndex;
+        fullToVisible[fullIndex] = visibleIndex;
+        fullIndex++;
+        visibleIndex++;
       }
 
-      return breakpoint.visible;
+      fullIndex = breakpoint.full;
+    }
+
+    while (fullIndex < fullLineCount) {
+      visibleToFull[visibleIndex] = fullIndex;
+      fullToVisible[fullIndex] = visibleIndex;
+      fullIndex++;
+      visibleIndex++;
     }
 
-    throw Exception(
-      'Never get here. upper == lower without finding a breakpoint',
+    return HiddenLineRanges._(
+      fullLineCount: fullLineCount,
+      breakpoints: breakpoints,
+      fullToVisible: fullToVisible,
+      visibleToFull: visibleToFull.sublist(0, visibleIndex),
     );
   }
 
-  Iterable<int> get visibleLineNumbers sync* {
-    int n = 0;
-
-    for (final breakpoint in breakpoints) {
-      final to = breakpoint.fullBefore;
+  const HiddenLineRanges._({
+    required this.breakpoints,
+    required this.fullLineCount,
+    required List<int?> fullToVisible,
+    required List<int> visibleToFull,
+  })  : _fullToVisible = fullToVisible,
+        _visibleToFull = visibleToFull;
 
-      while (n < to) {
-        yield n++;
-      }
+  static const empty = HiddenLineRanges._(
+    breakpoints: [],
+    fullLineCount: 1,
+    fullToVisible: [0],
+    visibleToFull: [0],
+  );
 
-      n = breakpoint.full;
-    }
+  int? cutLineIndexIfVisible(int lineIndex) {
+    return _fullToVisible[lineIndex];
+  }
 
-    while (n < fullLineCount) {
-      yield n++;
-    }
+  int recoverLineIndex(int visibleLineIndex) {
+    return _visibleToFull[visibleLineIndex];
   }
 
+  Iterable<int> get visibleLineNumbers => _visibleToFull;
+
   @override
   List<Object> get props => [
         breakpoints,
diff --git a/lib/src/highlight/node.dart b/lib/src/highlight/node.dart
index 2f4f31e..7c4ac85 100644
--- a/lib/src/highlight/node.dart
+++ b/lib/src/highlight/node.dart
@@ -51,4 +51,58 @@ extension MyNode on Node {
 
     return null;
   }
+
+  List<Node> splitLines() {
+    final splitValue = value?.split('\n');
+    if (splitValue == null || (splitValue.length) <= 1) {
+      return [copyWith(children: children?.splitLines())];
+    }
+    final result = <Node>[];
+    for (int i = 0; i < splitValue.length; i++) {
+      result.add(
+        copyWith(
+          value: splitValue[i] + (i == splitValue.length - 1 ? '' : '\n'),
+          children: i == 0 ? children?.splitLines() : null,
+        ),
+      );
+    }
+    return result;
+  }
+
+  String toStringRecursive([int indent = 0]) {
+    final sb = StringBuffer();
+    sb.writeln(' ' * 2 * indent + toMapString());
+    for (final child in children ?? const <Node>[]) {
+      sb.write(child.toStringRecursive(indent + 1));
+    }
+    return sb.toString();
+  }
+
+  String toMapString() {
+    final map = {
+      'value': value == null ? null : '\'$value\'',
+      'className': className,
+      'children': children == null ? 'empty' : null,
+    }..removeWhere((key, value) => value == null);
+    return map.toString();
+  }
+
+  Node copyWith({
+    String? className,
+    String? value,
+    bool? noPrefix,
+    List<Node>? children,
+  }) =>
+      Node(
+        className: className ?? this.className,
+        value: value ?? this.value,
+        noPrefix: noPrefix ?? this.noPrefix,
+        children: children ?? this.children,
+      );
+}
+
+extension NodeListExtension on List<Node> {
+  List<Node> splitLines() {
+    return map((n) => n.splitLines()).expand((e) => e).toList(growable: false);
+  }
 }
diff --git a/lib/src/highlight/result.dart b/lib/src/highlight/result.dart
index b47bc23..2c91d0f 100644
--- a/lib/src/highlight/result.dart
+++ b/lib/src/highlight/result.dart
@@ -40,4 +40,26 @@ extension MyResult on Result {
       characterIndex += node.getCharacterCount();
     }
   }
+
+  Result splitLines() {
+    return copyWith(
+      nodes: nodes?.splitLines(),
+    );
+  }
+
+  Result copyWith({
+    int? relevance,
+    List<Node>? nodes,
+    String? language,
+    Mode? top,
+    Result? secondBest,
+  }) {
+    return Result(
+      relevance: relevance ?? this.relevance,
+      nodes: nodes ?? this.nodes,
+      language: language ?? this.language,
+      top: top ?? this.top,
+      secondBest: secondBest ?? this.secondBest,
+    );
+  }
 }


Test patch:
diff --git a/test/src/code/code_lines_test.dart b/test/src/code/code_lines_test.dart
index aa0b6ec..3eb8853 100644
--- a/test/src/code/code_lines_test.dart
+++ b/test/src/code/code_lines_test.dart
@@ -21,7 +21,7 @@ void main() {
         expect(
           code.lines,
           CodeLines([
-            CodeLine(
+            CodeLine.fromTextAndRange(
               text: text,
               textRange: TextRange(start: 0, end: text.length),
               isReadOnly: false, // ignore: avoid_redundant_argument_values
diff --git a/test/src/code_field/span_builder_test.dart b/test/src/code_field/span_builder_test.dart
index 8bd40c0..b99cca0 100644
--- a/test/src/code_field/span_builder_test.dart
+++ b/test/src/code_field/span_builder_test.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables
+
 import 'package:flutter/widgets.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
 import 'package:flutter_code_editor/src/code_field/span_builder.dart';
@@ -5,12 +7,12 @@ import 'package:flutter_test/flutter_test.dart';
 import 'package:highlight/highlight_core.dart';
 import 'package:highlight/languages/java.dart';
 
-const _comment = TextStyle(color: Color(0x00000001));
-const _keyword = TextStyle(color: Color(0x00000002));
-const _class = TextStyle(color: Color(0x00000003));
-const _title = TextStyle(color: Color(0x00000004));
-const _function = TextStyle(color: Color(0x00000005));
-const _params = TextStyle(color: Color(0x00000006));
+const _comment = TextStyle(color: Color(0xFF000001));
+const _keyword = TextStyle(color: Color(0xFF000002));
+const _class = TextStyle(color: Color(0xFF000003));
+const _title = TextStyle(color: Color(0xFF000004));
+const _function = TextStyle(color: Color(0xFF000005));
+const _params = TextStyle(color: Color(0xFF000006));
 
 final _themeData = CodeThemeData(
   classStyle: _class,
@@ -59,7 +61,8 @@ public class MyClass {
                   TextSpan(text: ' '),
                 ],
               ),
-              TextSpan(text: '{\n  '),
+              TextSpan(text: '{\n'),
+              TextSpan(text: '  '),
               TextSpan(
                 style: _function,
                 children: [
@@ -91,7 +94,10 @@ public class MyClass {
                 style: _comment,
                 children: [TextSpan(text: '// comment')],
               ),
-              TextSpan(text: '\n  }\n}\n'),
+              TextSpan(text: '\n'),
+              TextSpan(text: '  }\n'),
+              TextSpan(text: '}\n'),
+              TextSpan(text: ''),
             ],
           ),
         ),
@@ -105,7 +111,7 @@ public class MyClass {
 }
 ''',
           mode: java,
-          expected: const TextSpan(
+          expected: TextSpan(
             children: [
               TextSpan(text: ''),
               TextSpan(
@@ -129,28 +135,29 @@ public class MyClass {
                   TextSpan(text: ' '),
                 ],
               ),
-              TextSpan(text: '{\n  '),
+              TextSpan(text: '{\n'),
+              TextSpan(text: '  '),
               TextSpan(
-                style: _function,
+                style: _function.paled(),
                 children: [
                   TextSpan(text: ''),
                   TextSpan(
-                    style: _keyword,
+                    style: _keyword.paled(),
                     children: [TextSpan(text: 'public')],
                   ),
                   TextSpan(text: ' '),
                   TextSpan(
-                    style: _keyword,
+                    style: _keyword.paled(),
                     children: [TextSpan(text: 'void')],
                   ),
                   TextSpan(text: ' '),
                   TextSpan(
-                    style: _title,
+                    style: _title.paled(),
                     children: [TextSpan(text: 'main')],
                   ),
                   TextSpan(text: ''),
                   TextSpan(
-                    style: _params,
+                    style: _params.paled(),
                     children: [TextSpan(text: '()')],
                   ),
                   TextSpan(text: ' '),
@@ -158,10 +165,13 @@ public class MyClass {
               ),
               TextSpan(text: '{ '),
               TextSpan(
-                style: _comment,
+                style: _comment.paled(),
                 children: [TextSpan(text: '')],
               ),
-              TextSpan(text: '\n  }\n}\n'),
+              TextSpan(text: '\n'),
+              TextSpan(text: '  }\n'),
+              TextSpan(text: '}\n'),
+              TextSpan(text: ''),
             ],
           ),
         ),
@@ -182,8 +192,8 @@ public class MyClass {
           language: mode,
         );
 
-        const builder = SpanBuilder();
-        final result = builder.build(code: code, theme: _themeData);
+        final builder = SpanBuilder(code: code, theme: _themeData);
+        final result = builder.build();
 
         expect(
           result,
diff --git a/test/src/code_field/split_highlight_lines_test.dart b/test/src/code_field/split_highlight_lines_test.dart
new file mode 100644
index 0000000..ea025d7
--- /dev/null
+++ b/test/src/code_field/split_highlight_lines_test.dart
@@ -0,0 +1,146 @@
+import 'package:flutter_code_editor/src/highlight/node.dart';
+import 'package:flutter_code_editor/src/highlight/result.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight.dart';
+import 'package:highlight/languages/dart.dart';
+
+const examples = {
+  //
+  'Empty': _Example(text: '', expected: '{value: \'\', children: empty}\n'),
+
+  'Contains comment': _Example(
+    text: '''
+public class MyClass {
+  public void main() { // comment
+  }
+}''',
+    expected: '''
+{value: 'public ', children: empty}
+{className: class}
+  {value: '', children: empty}
+  {className: keyword}
+    {value: 'class', children: empty}
+  {value: ' ', children: empty}
+  {className: title}
+    {value: 'MyClass', children: empty}
+  {value: ' ', children: empty}
+{value: '{\n', children: empty}
+{value: '  public ', children: empty}
+{className: keyword}
+  {value: 'void', children: empty}
+{value: ' main() { ', children: empty}
+{className: comment}
+  {value: '// comment', children: empty}
+{value: '\n', children: empty}
+{value: '  }\n', children: empty}
+{value: '}', children: empty}
+''',
+  ),
+
+  'Named sections': _Example(
+    text: '''
+class MyClass {
+  void method1() {// [START section1]
+  }// [END section1]
+  // [START section2]
+  void method2() {
+  }// [END section2]
+}''',
+    expected: '''
+{value: '', children: empty}
+{className: class}
+  {value: '', children: empty}
+  {className: keyword}
+    {value: 'class', children: empty}
+  {value: ' ', children: empty}
+  {className: title}
+    {value: 'MyClass', children: empty}
+  {value: ' ', children: empty}
+{value: '{\n', children: empty}
+{value: '  ', children: empty}
+{className: keyword}
+  {value: 'void', children: empty}
+{value: ' method1() {', children: empty}
+{className: comment}
+  {value: '// [START section1]', children: empty}
+{value: '\n', children: empty}
+{value: '  }', children: empty}
+{className: comment}
+  {value: '// [END section1]', children: empty}
+{value: '\n', children: empty}
+{value: '  ', children: empty}
+{className: comment}
+  {value: '// [START section2]', children: empty}
+{value: '\n', children: empty}
+{value: '  ', children: empty}
+{className: keyword}
+  {value: 'void', children: empty}
+{value: ' method2() {\n', children: empty}
+{value: '  }', children: empty}
+{className: comment}
+  {value: '// [END section2]', children: empty}
+{value: '\n', children: empty}
+{value: '}', children: empty}
+''',
+  ),
+
+  'Invalid code (Folded)': _Example(
+    text: '''
+class MyClass {
+  void method1() {
+
+  void method2() {
+}''',
+    expected: '''
+{value: '', children: empty}
+{className: class}
+  {value: '', children: empty}
+  {className: keyword}
+    {value: 'class', children: empty}
+  {value: ' ', children: empty}
+  {className: title}
+    {value: 'MyClass', children: empty}
+  {value: ' ', children: empty}
+{value: '{\n', children: empty}
+{value: '  ', children: empty}
+{className: keyword}
+  {value: 'void', children: empty}
+{value: ' method1() {\n', children: empty}
+{value: '\n', children: empty}
+{value: '  ', children: empty}
+{className: keyword}
+  {value: 'void', children: empty}
+{value: ' method2() {\n', children: empty}
+{value: '}', children: empty}
+''',
+  ),
+};
+
+void main() {
+  group('Highlight split lines test.', () {
+    highlight.registerLanguage('language', dart);
+    examples.forEach((name, example) {
+      test(name, () {
+        final highlighted = highlight.parse(example.text, language: 'language');
+
+        final splitLines = highlighted.splitLines();
+        final stringResult = splitLines.nodes
+            ?.map((element) => element.toStringRecursive())
+            .join();
+
+        expect(
+          stringResult,
+          example.expected,
+          reason: name,
+        );
+      });
+    });
+  });
+}
+
+class _Example {
+  final String text;
+  final String expected;
+
+  const _Example({required this.text, required this.expected});
+}
diff --git a/test/src/hidden_ranges/hidden_line_ranges_builder_test.dart b/test/src/hidden_ranges/hidden_line_ranges_builder_test.dart
index f5f1702..16df22a 100644
--- a/test/src/hidden_ranges/hidden_line_ranges_builder_test.dart
+++ b/test/src/hidden_ranges/hidden_line_ranges_builder_test.dart
@@ -26,7 +26,7 @@ void main() {
 
       expect(
         builder.hiddenLineRanges,
-        const HiddenLineRanges(
+        HiddenLineRanges(
           breakpoints: [],
           fullLineCount: 1,
         ),
@@ -41,7 +41,7 @@ void main() {
 
       expect(
         builder.hiddenLineRanges,
-        const HiddenLineRanges(
+        HiddenLineRanges(
           breakpoints: [],
           fullLineCount: 15,
         ),
@@ -81,7 +81,7 @@ void main() {
 
       expect(
         builder.hiddenLineRanges,
-        const HiddenLineRanges(
+        HiddenLineRanges(
           breakpoints: [],
           fullLineCount: 15,
         ),
@@ -121,8 +121,8 @@ void main() {
 
       expect(
         builder.hiddenLineRanges,
-        const HiddenLineRanges(
-          breakpoints: [
+        HiddenLineRanges(
+          breakpoints: const [
             LineNumberingBreakpoint(full: 6, visible: 4, spreadBefore: 0),
             LineNumberingBreakpoint(full: 14, visible: 10, spreadBefore: 2),
           ],
@@ -164,8 +164,8 @@ void main() {
 
       expect(
         builder.hiddenLineRanges,
-        const HiddenLineRanges(
-          breakpoints: [
+        HiddenLineRanges(
+          breakpoints: const [
             LineNumberingBreakpoint(full: 2, visible: 0, spreadBefore: 0),
             LineNumberingBreakpoint(full: 15, visible: 10, spreadBefore: 2),
           ],
diff --git a/test/src/hidden_ranges/hidden_line_ranges_test.dart b/test/src/hidden_ranges/hidden_line_ranges_test.dart
index 7f5f784..acbc60e 100644
--- a/test/src/hidden_ranges/hidden_line_ranges_test.dart
+++ b/test/src/hidden_ranges/hidden_line_ranges_test.dart
@@ -1,15 +1,22 @@
+import 'package:flutter_code_editor/src/code/code.dart';
 import 'package:flutter_code_editor/src/hidden_ranges/hidden_line_ranges.dart';
 import 'package:flutter_code_editor/src/hidden_ranges/line_numbering_breakpoint.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/languages/dart.dart';
 
 void main() {
-  const noBreakpointsRanges = HiddenLineRanges(
+  final emptyTextBreakpointRanges = HiddenLineRanges(
+    breakpoints: [],
+    fullLineCount: 1,
+  );
+
+  final noBreakpointsRanges = HiddenLineRanges(
     breakpoints: [],
     fullLineCount: 10,
   );
 
-  const midBreakpointsRanges = HiddenLineRanges(
-    breakpoints: [
+  final midBreakpointsRanges = HiddenLineRanges(
+    breakpoints: const [
       LineNumberingBreakpoint(full: 4, visible: 2, spreadBefore: 0),
       LineNumberingBreakpoint(full: 9, visible: 5, spreadBefore: 2),
       LineNumberingBreakpoint(full: 100, visible: 12, spreadBefore: 4),
@@ -20,8 +27,8 @@ void main() {
     fullLineCount: 231,
   );
 
-  const startEndHiddenRanges = HiddenLineRanges(
-    breakpoints: [
+  final startEndHiddenRanges = HiddenLineRanges(
+    breakpoints: const [
       LineNumberingBreakpoint(full: 3, visible: 0, spreadBefore: 0),
       LineNumberingBreakpoint(full: 10, visible: 5, spreadBefore: 3),
     ],
@@ -29,15 +36,25 @@ void main() {
   );
 
   group('HiddenLineRanges.', () {
-    group('cutLineIndexIfVisible', () {
+    test('Empty code generates correct line ranges', () {
+      final code = Code(text: '', language: dart);
+      expect(code.hiddenLineRanges.fullLineCount, 1);
+    });
+
+    group('cutLineIndexIfVisible, recoverLineIndex', () {
+      test('Empty text', () {
+        expect(emptyTextBreakpointRanges.cutLineIndexIfVisible(0), 0);
+        expect(emptyTextBreakpointRanges.recoverLineIndex(0), 0);
+      });
+
       test('No breakpoints -> Continuous', () {
-        for (int i = -1; i <= noBreakpointsRanges.fullLineCount; i++) {
+        for (int i = 0; i < noBreakpointsRanges.fullLineCount; i++) {
           expect(noBreakpointsRanges.cutLineIndexIfVisible(i), i);
+          expect(noBreakpointsRanges.recoverLineIndex(i), i);
         }
       });
 
       test('Mid breakpoints', () {
-        expect(midBreakpointsRanges.cutLineIndexIfVisible(-1), -1);
         expect(midBreakpointsRanges.cutLineIndexIfVisible(0), 0);
         expect(midBreakpointsRanges.cutLineIndexIfVisible(1), 1);
         expect(midBreakpointsRanges.cutLineIndexIfVisible(2), null);
@@ -55,10 +72,18 @@ void main() {
         expect(midBreakpointsRanges.cutLineIndexIfVisible(99), null);
         expect(midBreakpointsRanges.cutLineIndexIfVisible(100), 12);
         expect(midBreakpointsRanges.cutLineIndexIfVisible(101), 13);
-        expect(midBreakpointsRanges.cutLineIndexIfVisible(200), 112);
-        expect(midBreakpointsRanges.cutLineIndexIfVisible(209), null);
-        expect(midBreakpointsRanges.cutLineIndexIfVisible(219), null);
-        expect(midBreakpointsRanges.cutLineIndexIfVisible(229), null);
+
+        expect(midBreakpointsRanges.recoverLineIndex(0), 0);
+        expect(midBreakpointsRanges.recoverLineIndex(1), 1);
+        expect(midBreakpointsRanges.recoverLineIndex(2), 4);
+        expect(midBreakpointsRanges.recoverLineIndex(3), 5);
+        expect(midBreakpointsRanges.recoverLineIndex(4), 6);
+        expect(midBreakpointsRanges.recoverLineIndex(5), 9);
+        expect(midBreakpointsRanges.recoverLineIndex(6), 10);
+        expect(midBreakpointsRanges.recoverLineIndex(7), 11);
+        expect(midBreakpointsRanges.recoverLineIndex(11), 15);
+        expect(midBreakpointsRanges.recoverLineIndex(12), 100);
+        expect(midBreakpointsRanges.recoverLineIndex(13), 101);
       });
     });