Repository: akvelon/flutter-code-editor

Issue:
Named sections parsing
In code, there may be many names sections designated like this:

```
editable
[START section_name]
readonly
readonly
[END section_name]
editable
```

They will be used for read-only functionality, for initially collapsing blocks, and for hidden sections.
We need to parse them into `Code` object to get started on those features.



Fix patch:
diff --git a/lib/src/named_sections/named_section.dart b/lib/src/named_sections/named_section.dart
new file mode 100644
index 00000000..6cf2beeb
--- /dev/null
+++ b/lib/src/named_sections/named_section.dart
@@ -0,0 +1,32 @@
+import 'package:meta/meta.dart';
+
+@immutable
+class NamedSection {
+  /// Zero-based index of the line with the starting tag.
+  final int startLine;
+
+  /// Zero-based index of the line with the ending tag.
+  final int endLine;
+
+  final String name;
+
+  const NamedSection({
+    required this.startLine,
+    required this.endLine,
+    required this.name,
+  });
+
+  @override
+  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() => '$startLine-$endLine: "$name"';
+}
diff --git a/lib/src/named_sections/parsers/abstract.dart b/lib/src/named_sections/parsers/abstract.dart
new file mode 100644
index 00000000..96bed381
--- /dev/null
+++ b/lib/src/named_sections/parsers/abstract.dart
@@ -0,0 +1,44 @@
+import 'package:meta/meta.dart';
+
+import '../../single_line_comments/single_line_comment.dart';
+import '../named_section.dart';
+
+/// Parses named sections from tags like [START name]...[END name].
+///
+/// Subclasses must implement [parseUnsorted] for parsing.
+///
+/// This class does the following with the parsed sections:
+///
+/// 1. Validates. A section is invalid if any of these is true:
+///    - It starts before the line 0.
+///    - It ends after the last line (lineCount - 1).
+///    - The end line is before the start line.
+/// 2. Sorts by the start lines.
+abstract class AbstractNamedSectionParser {
+  List<NamedSection> parse({
+    required List<SingleLineComment> singleLineComments,
+    required int lineCount,
+  }) {
+    final unsorted = parseUnsorted(
+      singleLineComments: singleLineComments,
+      lineCount: lineCount,
+    );
+
+    return unsorted
+        .where((section) => _isValid(section, lineCount))
+        .toList(growable: false)
+      ..sort((a, b) => a.startLine - b.startLine);
+  }
+
+  @protected
+  List<NamedSection> parseUnsorted({
+    required List<SingleLineComment> singleLineComments,
+    required int lineCount,
+  });
+
+  bool _isValid(NamedSection section, int lineCount) {
+    return section.startLine >= 0 &&
+        section.endLine < lineCount &&
+        section.startLine <= section.endLine;
+  }
+}
diff --git a/lib/src/named_sections/parsers/brackets_start_end.dart b/lib/src/named_sections/parsers/brackets_start_end.dart
new file mode 100644
index 00000000..21f039ea
--- /dev/null
+++ b/lib/src/named_sections/parsers/brackets_start_end.dart
@@ -0,0 +1,89 @@
+import 'dart:math';
+
+import '../../single_line_comments/single_line_comment.dart';
+import '../named_section.dart';
+import 'abstract.dart';
+
+/// Parses named sections from tags like [START name]...[END name].
+///
+/// Section name may only contain english letters, numbers, and underscores.
+/// Section name must not be empty.
+/// Whitespaces are allowed before START/END, before name, and after name.
+///
+/// Sections can overlap and can be nested.
+///
+/// A section can start and end on the same line in a single comment or in two
+/// different comments.
+///
+/// If a section is never started, it starts at the line 0.
+/// If a section is never ended, it ends at the last line (count - 1).
+///
+/// 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.
+///
+/// The order of comments in the list does not matter.
+class BracketsStartEndNamedSectionParser extends AbstractNamedSectionParser {
+  static final _startRe = RegExp(r'\[(\s*)START(\s+)([_0-9a-zA-Z]+)(\s*)\]');
+  static final _endRe = RegExp(r'\[(\s*)END(\s+)([_0-9a-zA-Z]+)(\s*)\]');
+
+  @override
+  List<NamedSection> parseUnsorted({
+    required List<SingleLineComment> singleLineComments,
+    required int lineCount,
+  }) {
+    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) ?? '';
+        starts[name] = min(comment.lineIndex, starts[name] ?? lineCount - 1);
+      }
+
+      for (final match in _endRe.allMatches(comment.innerContent)) {
+        final name = match.group(3) ?? '';
+        ends[name] = max(comment.lineIndex, ends[name] ?? 0);
+      }
+    }
+
+    return _combineStartsAndEnds(starts, ends, lineCount);
+  }
+
+  List<NamedSection> _combineStartsAndEnds(
+    Map<String, int> starts,
+    Map<String, int> ends,
+    int lineCount,
+  ) {
+    final sections = <NamedSection>[];
+
+    for (final entry in ends.entries) {
+      final name = entry.key;
+      final end = entry.value;
+
+      sections.add(
+        NamedSection(
+          startLine: starts[name] ?? 0,
+          endLine: end,
+          name: name,
+        ),
+      );
+
+      starts.remove(name);
+    }
+
+    for (final entry in starts.entries) {
+      final name = entry.key;
+      final start = entry.value;
+
+      sections.add(
+        NamedSection(
+          startLine: start,
+          endLine: ends[name] ?? lineCount - 1,
+          name: name,
+        ),
+      );
+    }
+
+    return sections;
+  }
+}
diff --git a/lib/src/single_line_comments/single_line_comment.dart b/lib/src/single_line_comments/single_line_comment.dart
index cde67765..b856a3e5 100644
--- a/lib/src/single_line_comments/single_line_comment.dart
+++ b/lib/src/single_line_comments/single_line_comment.dart
@@ -14,7 +14,7 @@ class SingleLineComment {
   const SingleLineComment({
     required this.lineIndex,
     required this.innerContent,
-    required this.outerContent,
+    this.outerContent = '',
   });
 
   SingleLineComment.cut(
diff --git a/pubspec.yaml b/pubspec.yaml
index e7cefea2..2b4a4a3f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -22,7 +22,7 @@ dependencies:
 dev_dependencies:
   flutter_test:
     sdk: flutter
-  total_lints: ^2.17.0
+  total_lints: ^2.17.2
 
 flutter:
   uses-material-design: true


Test patch:
diff --git a/test/src/named_sections/parsers/brackets_start_end_test.dart b/test/src/named_sections/parsers/brackets_start_end_test.dart
new file mode 100644
index 00000000..aafdb0a9
--- /dev/null
+++ b/test/src/named_sections/parsers/brackets_start_end_test.dart
@@ -0,0 +1,103 @@
+import 'package:code_text_field/src/named_sections/named_section.dart';
+import 'package:code_text_field/src/named_sections/parsers/brackets_start_end.dart';
+import 'package:code_text_field/src/single_line_comments/single_line_comment.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('BracketsStartEndNamedSectionParser', () {
+    const comments = [
+      //
+      SingleLineComment(lineIndex: 13, innerContent: ' no named section'),
+
+      SingleLineComment(lineIndex: 14, innerContent: '[END never_started ]'),
+      SingleLineComment(lineIndex: 11, innerContent: '[START never_ending]'),
+
+      SingleLineComment(lineIndex: 16, innerContent: '[START outer]'),
+      SingleLineComment(lineIndex: 18, innerContent: '[START inner]'),
+      SingleLineComment(lineIndex: 21, innerContent: '[END inner]'),
+      SingleLineComment(lineIndex: 21, innerContent: '[END outer]'),
+
+      SingleLineComment(lineIndex: 80, innerContent: '[START line][END line]'),
+      SingleLineComment(lineIndex: 81, innerContent: '[END swap][START swap]'),
+      SingleLineComment(lineIndex: 82, innerContent: '[START split_line]'),
+      SingleLineComment(lineIndex: 82, innerContent: '[ END split_line]'),
+      SingleLineComment(lineIndex: 83, innerContent: '[START 2]'),
+      SingleLineComment(lineIndex: 84, innerContent: '[START _]'),
+
+      SingleLineComment(lineIndex: 71, innerContent: 'a [START text_before]'),
+      SingleLineComment(lineIndex: 72, innerContent: 'a [END text_before]'),
+      SingleLineComment(lineIndex: 73, innerContent: '[START text_after] b'),
+      SingleLineComment(lineIndex: 74, innerContent: '[END text_after] b'),
+
+      SingleLineComment(lineIndex: 35, innerContent: '[END overlapping1]'),
+      SingleLineComment(lineIndex: 30, innerContent: '[START overlapping2]'),
+      SingleLineComment(lineIndex: 24, innerContent: '[START overlapping1]'),
+      SingleLineComment(lineIndex: 40, innerContent: '[END  overlapping2]'),
+
+      SingleLineComment(lineIndex: 50, innerContent: '[START no-dashes]'),
+      SingleLineComment(lineIndex: 51, innerContent: '[START no spaces]'),
+      SingleLineComment(lineIndex: 52, innerContent: 'START no_bracket1]'),
+      SingleLineComment(lineIndex: 53, innerContent: '[START no_bracket2'),
+      SingleLineComment(lineIndex: 54, innerContent: '[!START nothing_before]'),
+      SingleLineComment(lineIndex: 55, innerContent: '[END nothing_after!]'),
+      SingleLineComment(lineIndex: 56, innerContent: '[ END ]'),
+
+      SingleLineComment(lineIndex: 30, innerContent: '[Start non_all_caps]'),
+      SingleLineComment(lineIndex: 30, innerContent: '[End non_all_caps]'),
+
+      SingleLineComment(lineIndex: 41, innerContent: '[START reversed]'),
+      SingleLineComment(lineIndex: 40, innerContent: '[END reversed]'),
+
+      SingleLineComment(lineIndex: 42, innerContent: '[START duplicate_start]'),
+      SingleLineComment(lineIndex: 41, innerContent: '[START duplicate_start]'),
+      SingleLineComment(lineIndex: 45, innerContent: '[END duplicate_start]'),
+      SingleLineComment(lineIndex: 43, innerContent: '[START duplicate_start]'),
+
+      SingleLineComment(lineIndex: 61, innerContent: '[START duplicate_end]'),
+      SingleLineComment(lineIndex: 65, innerContent: '[END duplicate_end]'),
+      SingleLineComment(lineIndex: 67, innerContent: '[END duplicate_end]'),
+      SingleLineComment(lineIndex: 65, innerContent: '[END duplicate_end]'),
+
+      SingleLineComment(lineIndex: 72, innerContent: '[START START]'),
+      SingleLineComment(lineIndex: 73, innerContent: '[END START]'),
+      SingleLineComment(lineIndex: 74, innerContent: '[START END]'),
+      SingleLineComment(lineIndex: 75, innerContent: '[END END]'),
+
+      SingleLineComment(lineIndex: -3, innerContent: '[START before0]'),
+      SingleLineComment(lineIndex: -2, innerContent: '[END before0]'),
+      SingleLineComment(lineIndex: 98, innerContent: '[START after_eof]'),
+      SingleLineComment(lineIndex: 99, innerContent: '[END after_eof]'),
+    ];
+
+    const lineCount = 95;
+    const last = lineCount - 1;
+
+    const expected = [
+      NamedSection(startLine: 0, endLine: 14, name: 'never_started'),
+      NamedSection(startLine: 11, endLine: last, 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: last, name: '2'),
+      NamedSection(startLine: 84, endLine: last, name: '_'),
+    ];
+
+    final parser = BracketsStartEndNamedSectionParser();
+    final parsed = parser.parse(
+      singleLineComments: comments,
+      lineCount: lineCount,
+    );
+
+    expect(parsed, expected);
+  });
+}