Repository: akvelon/flutter-code-editor
Pull request #104 (2022-11-02 09:06:49)

Issue27 visible sections


Patch:
diff --git a/lib/src/code/code.dart b/lib/src/code/code.dart
index cc7461c..98f2846 100644
--- a/lib/src/code/code.dart
+++ b/lib/src/code/code.dart
@@ -23,6 +23,7 @@ import 'code_edit_result.dart';
 import 'code_line.dart';
 import 'code_lines.dart';
 import 'code_lines_builder.dart';
+import 'map.dart';
 import 'string.dart';
 import 'text_range.dart';
 
@@ -38,6 +39,7 @@ class Code {
   final Map<String, NamedSection> namedSections;
   final Result? visibleHighlighted;
   final String visibleText;
+  final Set<String> visibleSectionNames;
 
   final HiddenRangesBuilder _hiddenRangesBuilder;
 
@@ -47,6 +49,7 @@ class Code {
     Mode? language,
     AbstractNamedSectionParser? namedSectionParser,
     Set<String> readOnlySectionNames = const {},
+    Set<String> visibleSectionNames = const {},
   }) {
     final sequences = SingleLineComments.byMode[language] ?? [];
 
@@ -92,15 +95,31 @@ class Code {
         const [];
     final sectionsMap = {for (final s in sections) s.name: s};
 
-    _applyNamedSectionsToLines(
-      lines: lines.lines,
-      sections: sectionsMap,
-      readOnlySectionNames: readOnlySectionNames,
+    final isCodeReadonly = visibleSectionNames.isNotEmpty;
+    if (isCodeReadonly) {
+      _makeCodeReadonly(lines: lines.lines);
+    } else {
+      _applyNamedSectionsToLines(
+        lines: lines.lines,
+        sections: sectionsMap,
+        readOnlySectionNames: readOnlySectionNames,
+      );
+    }
+
+    final visibleSectionsHiddenRanges = _visibleSectionsToHiddenRanges(
+      visibleSectionNames,
+      sectionsMap,
+      lines,
+    );
+
+    final commentsHiddenRanges = _commentsToHiddenRanges(
+      serviceComments,
     );
 
     final hiddenRangesBuilder = HiddenRangesBuilder.fromMaps(
       {
-        int: _commentsToHiddenRanges(serviceComments),
+        String: visibleSectionsHiddenRanges,
+        int: commentsHiddenRanges,
       },
       textLength: text.length,
     );
@@ -124,6 +143,7 @@ class Code {
       namedSections: sectionsMap,
       visibleHighlighted: hiddenRanges.cutHighlighted(highlighted),
       visibleText: hiddenRanges.cutString(text),
+      visibleSectionNames: visibleSectionNames,
     );
   }
 
@@ -140,6 +160,7 @@ class Code {
     required this.namedSections,
     required this.visibleHighlighted,
     required this.visibleText,
+    required this.visibleSectionNames,
   }) : _hiddenRangesBuilder = hiddenRangesBuilder;
 
   static const empty = Code._(
@@ -155,8 +176,17 @@ class Code {
     namedSections: {},
     visibleHighlighted: null,
     visibleText: '',
+    visibleSectionNames: {},
   );
 
+  static void _makeCodeReadonly({
+    required List<CodeLine> lines,
+  }) {
+    for (int i = 0; i < lines.length; i++) {
+      lines[i] = lines[i].copyWith(isReadOnly: true);
+    }
+  }
+
   static void _applyNamedSectionsToLines({
     required List<CodeLine> lines,
     required Map<String, NamedSection> sections,
@@ -177,6 +207,54 @@ class Code {
     }
   }
 
+  ///Assumes that there is only one visible section.
+  ///If there are more than one, the first one will be used,
+  ///and the rest will be ignored.
+  static Map<String, HiddenRange> _visibleSectionsToHiddenRanges(
+    Set<String> names,
+    Map<String, NamedSection> namedSections,
+    CodeLines lines,
+  ) {
+    final section = namedSections.getByKeys(names).firstOrNull;
+
+    final result = <String, HiddenRange>{};
+
+    if (section == null) {
+      if (lines.lines.last.textRange.end > 0 && names.isNotEmpty) {
+        result['beginning'] = HiddenRange(
+          0,
+          lines.lines.last.textRange.end,
+          firstLine: 0,
+          lastLine: lines.lines.length - 1,
+          wholeFirstLine: true,
+        );
+      }
+      return result;
+    }
+
+    final startLine = lines.lines[section.firstLine];
+    if (startLine.textRange.start > 0) {
+      result['beginning'] = HiddenRange(
+        0,
+        startLine.textRange.start,
+        firstLine: 0,
+        lastLine: section.firstLine - 1,
+        wholeFirstLine: true,
+      );
+    }
+    final lastLine = section.lastLine;
+    if (lastLine != null && lastLine < lines.lines.length - 1) {
+      result['ending'] = HiddenRange(
+        lines.lines[lastLine].textRange.end,
+        lines.lines.last.textRange.end,
+        firstLine: lastLine,
+        lastLine: lines.lines.length - 1,
+        wholeFirstLine: false,
+      );
+    }
+    return result;
+  }
+
   static Map<int, HiddenRange> _commentsToHiddenRanges(
     Iterable<SingleLineComment> comments,
   ) {
@@ -413,6 +491,7 @@ class Code {
       namedSections: namedSections,
       visibleHighlighted: hiddenRanges.cutHighlighted(highlighted),
       visibleText: hiddenRanges.cutString(text),
+      visibleSectionNames: visibleSectionNames,
     );
   }
 }
diff --git a/lib/src/code/map.dart b/lib/src/code/map.dart
new file mode 100644
index 0000000..571f4df
--- /dev/null
+++ b/lib/src/code/map.dart
@@ -0,0 +1,28 @@
+extension MapExtension<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!);
+    }
+  }
+
+  List<V> getByKeys(Iterable<K> keys) {
+    final result = <V>[];
+    for (final key in keys) {
+      final value = this[key];
+      if (value != null) {
+        result.add(value);
+      }
+    }
+    return result;
+  }
+}
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index ec9be44..ea97f59 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -97,6 +97,10 @@ class CodeController extends TextEditingController {
   /* Computed members */
   String _languageId = _genId();
 
+  ///Contains names of named sections, those will be visible for user.
+  ///If it is not empty, all another code except specified will be hidden.
+  Set<String> _visibleSectionNames = {};
+
   String get languageId => _languageId;
 
   Code _code;
@@ -120,6 +124,7 @@ class CodeController extends TextEditingController {
     Mode? language,
     this.namedSectionParser,
     Set<String> readOnlySectionNames = const {},
+    Set<String> visibleSectionNames = const {},
     @Deprecated('Use CodeTheme widget to provide theme to CodeField.')
         Map<String, TextStyle>? theme,
     this.patternMap,
@@ -137,6 +142,7 @@ class CodeController extends TextEditingController {
         _code = Code.empty,
         _isTabReplacementEnabled = modifiers.any((e) => e is TabModifier) {
     this.language = language;
+    this.visibleSectionNames = visibleSectionNames;
     _code = _createCode(text ?? '');
     fullText = text ?? '';
 
@@ -416,6 +422,7 @@ class CodeController extends TextEditingController {
       highlighted: highlight.parse(rawText, language: _languageId),
       namedSectionParser: namedSectionParser,
       readOnlySectionNames: _readOnlySectionNames,
+      visibleSectionNames: _visibleSectionNames,
     );
   }
 
@@ -476,25 +483,36 @@ class CodeController extends TextEditingController {
   }
 
   void foldAt(int line) {
-    final oldCode = _code;
-    _code = _code.foldedAt(line);
+    final newCode = _code.foldedAt(line);
+    super.value = _getValueWithCode(newCode);
 
-    super.value = TextEditingValue(
-      text: _code.visibleText,
-      selection: _code.hiddenRanges.cutSelection(
-        oldCode.hiddenRanges.recoverSelection(value.selection),
-      ),
-    );
+    _code = newCode;
   }
 
   void unfoldAt(int line) {
-    final oldCode = _code;
-    _code = _code.unfoldedAt(line);
+    final newCode = _code.unfoldedAt(line);
+    super.value = _getValueWithCode(newCode);
 
-    super.value = TextEditingValue(
-      text: _code.visibleText,
-      selection: _code.hiddenRanges.cutSelection(
-        oldCode.hiddenRanges.recoverSelection(value.selection),
+    _code = newCode;
+  }
+
+  Set<String> get visibleSectionNames => _visibleSectionNames;
+
+  set visibleSectionNames(Set<String> sectionNames) {
+    _visibleSectionNames = sectionNames;
+    _updateCode(_code.text);
+
+    super.value = _getValueWithCode(_code);
+  }
+
+
+
+  /// The value with [newCode] preserving the current selection.
+  TextEditingValue _getValueWithCode(Code newCode) {
+    return TextEditingValue(
+      text: newCode.visibleText,
+      selection: newCode.hiddenRanges.cutSelection(
+        _code.hiddenRanges.recoverSelection(value.selection),
       ),
     );
   }
diff --git a/lib/src/folding/parsers/indent.dart b/lib/src/folding/parsers/indent.dart
index 74b3936..259f5cb 100644
--- a/lib/src/folding/parsers/indent.dart
+++ b/lib/src/folding/parsers/indent.dart
@@ -2,6 +2,7 @@ import 'package:highlight/highlight_core.dart';
 
 import '../../code/code_line.dart';
 import '../../code/code_lines.dart';
+import '../../code/map.dart';
 import '../foldable_block.dart';
 import '../foldable_block_type.dart';
 import 'abstract.dart';
@@ -157,21 +158,3 @@ class _SignificantIndentIndexes {
 
   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/hidden_ranges/hidden_range.dart b/lib/src/hidden_ranges/hidden_range.dart
index 7e462cc..4219f67 100644
--- a/lib/src/hidden_ranges/hidden_range.dart
+++ b/lib/src/hidden_ranges/hidden_range.dart
@@ -5,6 +5,8 @@ import '../code/text_range.dart';
 class HiddenRange extends NormalizedTextRange with EquatableMixin {
   final int firstLine;
   final int lastLine;
+
+  ///Whether this range contains its first line entirely.
   final bool wholeFirstLine;
 
   const HiddenRange(
diff --git a/lib/src/hidden_ranges/hidden_ranges.dart b/lib/src/hidden_ranges/hidden_ranges.dart
index 0128fed..050b480 100644
--- a/lib/src/hidden_ranges/hidden_ranges.dart
+++ b/lib/src/hidden_ranges/hidden_ranges.dart
@@ -307,6 +307,8 @@ class HiddenRanges {
     }
   }
 
+  /// Translates the [selection] of the full text 
+  /// to selection of the visible text.
   TextSelection cutSelection(TextSelection selection) {
     if (selection.isCollapsed) {
       final position = cutPosition(selection.start);
@@ -322,6 +324,8 @@ class HiddenRanges {
     );
   }
 
+  /// Translates the [selection] of the visible text
+  /// to selection of the full text.
   TextSelection recoverSelection(TextSelection selection) {
     if (selection.isCollapsed) {
       final position = recoverPosition(


Test patch:
diff --git a/test/src/code_field/code_controller_visible_sections_test.dart b/test/src/code_field/code_controller_visible_sections_test.dart
new file mode 100644
index 0000000..8101138
--- /dev/null
+++ b/test/src/code_field/code_controller_visible_sections_test.dart
@@ -0,0 +1,228 @@
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_code_editor/src/code_field/code_controller.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/languages/dart.dart';
+
+import '../common/create_app.dart';
+
+const _fullText = '''
+class MyClass {//    0 [START whole]
+  //                 1 [START separate_start_no_end][START separate_start_separate_end][START separate_start_trailing_end]
+  void method() {//  2 [START trailing_start_no_end][START trailing_start_separate_end][START trailing_start_trailing_end]
+  }//                3 [END separate_start_trailing_end][END trailing_start_trailing_end][END no_start_trailing_end]
+  //                 4 [END separate_start_separate_end][END trailing_start_separate_end][END no_start_separate_end]
+}//                  5 [END whole]''';
+
+const _fullVisibleText = '''
+class MyClass {
+  
+  void method() {
+  }
+  
+}''';
+
+const _method = '''
+  void method() {
+  }''';
+
+void main() {
+  CodeController createTestController(Set<String> visibleSections) =>
+      createController(
+        _fullText,
+        language: dart,
+        visibleSectionNames: visibleSections,
+      );
+
+  group('Visible sections', () {
+    test('Non existent visible section', () {
+      final controller = createTestController({'while'});
+      expect(controller.value.text, '');
+      expect(controller.code.hiddenLineRanges.visibleLineNumbers.toList(), [5]);
+    });
+
+    test('Whole text as visible section', () {
+      final controller = createTestController({'whole'});
+
+      expect(controller.value.text, _fullVisibleText);
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [0, 1, 2, 3, 4, 5],
+      );
+    });
+
+    test('Separate start no end', () {
+      final controller = createTestController({'separate_start_no_end'});
+
+      expect(controller.value.text, '''
+  
+$_method
+  
+}''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [1, 2, 3, 4, 5],
+      );
+    });
+
+    test('Separate start separate end', () {
+      final controller = createTestController({'separate_start_separate_end'});
+
+      expect(controller.value.text, '''
+  
+$_method
+  
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [1, 2, 3, 4],
+      );
+    });
+
+    test('Separate start trailing end', () {
+      final controller = createTestController({'separate_start_trailing_end'});
+
+      expect(controller.value.text, '''
+  
+$_method
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [1, 2, 3],
+      );
+    });
+
+    test('Trailing start no end', () {
+      final controller = createTestController({'trailing_start_no_end'});
+
+      expect(controller.value.text, '''
+$_method
+  
+}''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [2, 3, 4, 5],
+      );
+    });
+
+    test('Trailing start separate end', () {
+      final controller = createTestController({'trailing_start_separate_end'});
+
+      expect(controller.value.text, '''
+$_method
+  
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [2, 3, 4],
+      );
+    });
+
+    test('Trailing start trailing end', () {
+      final controller = createTestController({'trailing_start_trailing_end'});
+
+      expect(controller.value.text, '''
+$_method
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [2, 3],
+      );
+    });
+
+    test('No start separate end', () {
+      final controller = createTestController({'no_start_separate_end'});
+
+      expect(controller.value.text, '''
+class MyClass {
+  
+$_method
+  
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [0, 1, 2, 3, 4],
+      );
+    });
+
+    test('No start trailing end', () {
+      final controller = createTestController({'no_start_trailing_end'});
+
+      expect(controller.value.text, '''
+class MyClass {
+  
+$_method
+''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [0, 1, 2, 3],
+      );
+    });
+
+    test('Block folding works correctly', () {
+      final controller = createTestController({'whole'});
+      controller.foldAt(2);
+
+      expect(controller.value.text, '''
+class MyClass {
+  
+  void method() {
+  
+}''');
+      expect(
+        controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+        [0, 1, 2, 4, 5],
+      );
+    });
+
+    test('Code containing visible sections is readonly', () {
+      final controller = createTestController({'whole'});
+
+      final newLineIndexes = '\n'.allMatches(_fullVisibleText).toList();
+      for (int i = 0; i < newLineIndexes.length; i++) {
+        final index = newLineIndexes[i].start;
+        // ignore: prefer_interpolation_to_compose_strings
+        final newText = _fullVisibleText.substring(0, index) +
+            'some text' +
+            _fullVisibleText.substring(index);
+        controller.value = TextEditingValue(
+          text: newText,
+          selection: TextSelection.collapsed(offset: index),
+        );
+
+        expect(controller.value.text, _fullVisibleText);
+        expect(
+          controller.code.hiddenLineRanges.visibleLineNumbers.toList(),
+          [0, 1, 2, 3, 4, 5],
+        );
+      }
+    });
+
+    test('Code folding works with changing visible sections', () {
+      final controller = createController(
+        '''
+void method1() {// [START method1]
+  print('method1');
+}// [END method1]
+
+void method2() {// [START method2]
+  print('method2');
+}// [END method2]''',
+        language: dart,
+      );
+
+      controller.foldAt(4);
+      controller.visibleSectionNames = {'method2'};
+      expect(controller.value.text, 'void method2() {');
+
+      controller.visibleSectionNames = {};
+      expect(controller.value.text, '''
+void method1() {
+  print('method1');
+}
+
+void method2() {''');
+    });
+  });
+}
diff --git a/test/src/common/create_app.dart b/test/src/common/create_app.dart
index 65f6db0..ef5c413 100644
--- a/test/src/common/create_app.dart
+++ b/test/src/common/create_app.dart
@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/highlight.dart';
 import 'package:highlight/languages/java.dart';
 
 FocusNode focusNode = FocusNode();
@@ -26,10 +27,15 @@ Future<CodeController> pumpController(WidgetTester wt, String text) async {
   return controller;
 }
 
-CodeController createController(String text) {
+CodeController createController(
+  String text, {
+  Mode? language,
+  Set<String> visibleSectionNames = const {},
+}) {
   return CodeController(
     text: text,
-    language: java,
+    language: language ?? java,
     namedSectionParser: const BracketsStartEndNamedSectionParser(),
+    visibleSectionNames: visibleSectionNames,
   );
 }