Repository: akvelon/flutter-code-editor
Pull request #121 (2022-11-16 07:43:20)

Issue120 fix readonly bugs


Patch:
diff --git a/lib/src/code/text_span.dart b/lib/src/code/text_span.dart
new file mode 100644
index 0000000..38dafc7
--- /dev/null
+++ b/lib/src/code/text_span.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/painting.dart';
+
+import 'text_style.dart';
+
+extension TextSpanExtension on TextSpan {
+  String toStringRecursive([int indent = 0]) {
+    final sb = StringBuffer();
+    sb.writeln(' ' * 2 * indent + toMapString());
+    for (final child in children ?? const <TextSpan>[]) {
+      sb.write((child as TextSpan).toStringRecursive(indent + 1));
+    }
+    return sb.toString();
+  }
+
+  String toMapString() {
+    final result = {
+      'text': text == null ? null : '\'$text\'',
+      'style': style?.toMapString(),
+      'children': children == null ? 'empty' : null,
+    };
+    result.removeWhere((key, value) => value == null);
+    return result.toString();
+  }
+}
diff --git a/lib/src/code/text_style.dart b/lib/src/code/text_style.dart
new file mode 100644
index 0000000..a5721c0
--- /dev/null
+++ b/lib/src/code/text_style.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/painting.dart';
+
+extension TextStyleExtension on TextStyle {
+  String toMapString() {
+    final result = {
+      'color': color,
+      //add another fields if required
+    };
+    result.removeWhere((key, value) => value == null);
+    return result.toString();
+  }
+
+  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/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index 0da5c1c..7ced924 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -558,7 +558,7 @@ class CodeController extends TextEditingController {
       return SpanBuilder(
         code: _code,
         theme: _getTheme(context),
-        textStyle: style,
+        rootStyle: style,
       ).build();
     }
 
diff --git a/lib/src/code_field/span_builder.dart b/lib/src/code_field/span_builder.dart
index cc3533c..1bc7974 100644
--- a/lib/src/code_field/span_builder.dart
+++ b/lib/src/code_field/span_builder.dart
@@ -1,30 +1,32 @@
 import 'package:flutter/widgets.dart';
 import 'package:highlight/highlight_core.dart';
 
-import '../../src/highlight/node.dart';
 import '../code/code.dart';
+import '../code/text_style.dart';
 import '../code_theme/code_theme_data.dart';
+import '../highlight/node.dart';
 
 class SpanBuilder {
   final Code code;
   final CodeThemeData? theme;
-  final TextStyle? textStyle;
+  final TextStyle? rootStyle;
 
   int _visibleLineIndex = 0;
 
   SpanBuilder({
     required this.code,
     required this.theme,
-    this.textStyle,
+    this.rootStyle,
   });
 
   TextSpan build() {
     _visibleLineIndex = 0;
     return TextSpan(
-      style: textStyle,
+      style: rootStyle,
       children: _buildList(
         nodes: code.visibleHighlighted?.nodes ?? [],
         theme: theme,
+        ancestorStyle: rootStyle
       ),
     );
   }
@@ -32,36 +34,46 @@ class SpanBuilder {
   List<TextSpan>? _buildList({
     required List<Node>? nodes,
     required CodeThemeData? theme,
+    TextStyle? ancestorStyle,
   }) {
     if (nodes == null) {
       return null;
     }
 
     return nodes
-        .map((node) => _buildNode(node: node, theme: theme))
+        .map(
+          (node) => _buildNode(
+            node: node,
+            theme: theme,
+            ancestorStyle: ancestorStyle,
+          ),
+        )
         .toList(growable: false);
   }
 
   TextSpan _buildNode({
     required Node node,
     required CodeThemeData? theme,
+    TextStyle? ancestorStyle,
   }) {
-    _updateLineIndex(node);
+    final style = theme?.styles[node.className] ?? ancestorStyle;
+    final processedStyle = _paleIfRequired(style);
 
-    final style = theme?.styles[node.className];
+    _updateLineIndex(node);
 
     return TextSpan(
       text: node.value,
       children: _buildList(
         nodes: node.children,
         theme: theme,
+        ancestorStyle: style,
       ),
-      style: _paleIfRequired(style),
+      style: processedStyle,
     );
   }
 
   void _updateLineIndex(Node node) {
-    _visibleLineIndex += node.getNewlineCount();
+    _visibleLineIndex += node.getValueNewlineCount();
 
     if (_visibleLineIndex >= code.lines.length) {
       _visibleLineIndex = code.lines.length - 1;
@@ -72,27 +84,8 @@ class SpanBuilder {
     final fullLineIndex =
         code.hiddenLineRanges.recoverLineIndex(_visibleLineIndex);
     if (code.lines[fullLineIndex].isReadOnly) {
-      return (style ?? textStyle)?.paled();
+      return style?.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,
-      ),
-    );
-  }
-}


Test patch:
diff --git a/test/src/code_field/span_builder_test.dart b/test/src/code_field/span_builder_test.dart
index b99cca0..0365e29 100644
--- a/test/src/code_field/span_builder_test.dart
+++ b/test/src/code_field/span_builder_test.dart
@@ -2,11 +2,14 @@
 
 import 'package:flutter/widgets.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:flutter_code_editor/src/code/text_style.dart';
 import 'package:flutter_code_editor/src/code_field/span_builder.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:highlight/highlight_core.dart';
+import 'package:highlight/languages/dart.dart';
 import 'package:highlight/languages/java.dart';
 
+const _default = TextStyle(color: Color(0xFF000000));
 const _comment = TextStyle(color: Color(0xFF000001));
 const _keyword = TextStyle(color: Color(0xFF000002));
 const _class = TextStyle(color: Color(0xFF000003));
@@ -23,161 +26,212 @@ final _themeData = CodeThemeData(
   paramsStyle: _params,
 );
 
-void main() {
-  group('SpanBuilder', () {
-    test('builds highlighted', () {
-      final examples = [
-        //
-        _Example(
-          'Ordinary comments are visible',
-          text: '''
+final _examples = {
+  //
+  'Ordinary comments are visible': _Example(
+    text: '''
 public class MyClass {
   public void main() { // comment
   }
 }
 ''',
-          mode: java,
-          expected: const TextSpan(
-            children: [
-              TextSpan(text: ''),
-              TextSpan(
-                style: _keyword,
-                children: [TextSpan(text: 'public')],
-              ),
-              TextSpan(text: ' '),
-              TextSpan(
-                style: _class,
-                children: [
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _keyword,
-                    children: [TextSpan(text: 'class')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _title,
-                    children: [TextSpan(text: 'MyClass')],
-                  ),
-                  TextSpan(text: ' '),
-                ],
-              ),
-              TextSpan(text: '{\n'),
-              TextSpan(text: '  '),
-              TextSpan(
-                style: _function,
-                children: [
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _keyword,
-                    children: [TextSpan(text: 'public')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _keyword,
-                    children: [TextSpan(text: 'void')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _title,
-                    children: [TextSpan(text: 'main')],
-                  ),
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _params,
-                    children: [TextSpan(text: '()')],
-                  ),
-                  TextSpan(text: ' '),
-                ],
-              ),
-              TextSpan(text: '{ '),
-              TextSpan(
-                style: _comment,
-                children: [TextSpan(text: '// comment')],
-              ),
-              TextSpan(text: '\n'),
-              TextSpan(text: '  }\n'),
-              TextSpan(text: '}\n'),
-              TextSpan(text: ''),
-            ],
-          ),
+    mode: java,
+    expected: const TextSpan(
+      style: _default,
+      children: [
+        TextSpan(text: '', style: _default),
+        TextSpan(
+          style: _keyword,
+          children: [TextSpan(text: 'public', style: _keyword)],
+        ),
+        TextSpan(text: ' ', style: _default),
+        TextSpan(
+          style: _class,
+          children: [
+            TextSpan(text: '', style: _class),
+            TextSpan(
+              style: _keyword,
+              children: [TextSpan(text: 'class', style: _keyword)],
+            ),
+            TextSpan(text: ' ', style: _class),
+            TextSpan(
+              style: _title,
+              children: [TextSpan(text: 'MyClass', style: _title)],
+            ),
+            TextSpan(text: ' ', style: _class),
+          ],
+        ),
+        TextSpan(text: '{\n', style: _default),
+        TextSpan(text: '  ', style: _default),
+        TextSpan(
+          style: _function,
+          children: [
+            TextSpan(text: '', style: _function),
+            TextSpan(
+              style: _keyword,
+              children: [TextSpan(text: 'public', style: _keyword)],
+            ),
+            TextSpan(text: ' ', style: _function),
+            TextSpan(
+              style: _keyword,
+              children: [TextSpan(text: 'void', style: _keyword)],
+            ),
+            TextSpan(text: ' ', style: _function),
+            TextSpan(
+              style: _title,
+              children: [TextSpan(text: 'main', style: _title)],
+            ),
+            TextSpan(text: '', style: _function),
+            TextSpan(
+              style: _params,
+              children: [TextSpan(text: '()', style: _params)],
+            ),
+            TextSpan(text: ' ', style: _function),
+          ],
         ),
+        TextSpan(text: '{ ', style: _default),
+        TextSpan(
+          style: _comment,
+          children: [TextSpan(text: '// comment', style: _comment)],
+        ),
+        TextSpan(text: '\n', style: _default),
+        TextSpan(text: '  }\n', style: _default),
+        TextSpan(text: '}\n', style: _default),
+        TextSpan(text: '', style: _default),
+      ],
+    ),
+  ),
 
-        _Example(
-          'Service comments are hidden',
-          text: '''
+  'Service comments are hidden': _Example(
+    text: '''
 public class MyClass {
   public void main() { // readonly
   }
 }
 ''',
-          mode: java,
-          expected: TextSpan(
-            children: [
-              TextSpan(text: ''),
-              TextSpan(
-                style: _keyword,
-                children: [TextSpan(text: 'public')],
-              ),
-              TextSpan(text: ' '),
-              TextSpan(
-                style: _class,
-                children: [
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _keyword,
-                    children: [TextSpan(text: 'class')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _title,
-                    children: [TextSpan(text: 'MyClass')],
-                  ),
-                  TextSpan(text: ' '),
-                ],
-              ),
-              TextSpan(text: '{\n'),
-              TextSpan(text: '  '),
-              TextSpan(
-                style: _function.paled(),
-                children: [
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _keyword.paled(),
-                    children: [TextSpan(text: 'public')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _keyword.paled(),
-                    children: [TextSpan(text: 'void')],
-                  ),
-                  TextSpan(text: ' '),
-                  TextSpan(
-                    style: _title.paled(),
-                    children: [TextSpan(text: 'main')],
-                  ),
-                  TextSpan(text: ''),
-                  TextSpan(
-                    style: _params.paled(),
-                    children: [TextSpan(text: '()')],
-                  ),
-                  TextSpan(text: ' '),
-                ],
-              ),
-              TextSpan(text: '{ '),
-              TextSpan(
-                style: _comment.paled(),
-                children: [TextSpan(text: '')],
-              ),
-              TextSpan(text: '\n'),
-              TextSpan(text: '  }\n'),
-              TextSpan(text: '}\n'),
-              TextSpan(text: ''),
-            ],
-          ),
+    mode: java,
+    expected: TextSpan(
+      style: _default,
+      children: [
+        TextSpan(text: '', style: _default),
+        TextSpan(
+          style: _keyword,
+          children: [TextSpan(text: 'public', style: _keyword)],
+        ),
+        TextSpan(text: ' ', style: _default),
+        TextSpan(
+          style: _class,
+          children: [
+            TextSpan(text: '', style: _class),
+            TextSpan(
+              style: _keyword,
+              children: [TextSpan(text: 'class', style: _keyword)],
+            ),
+            TextSpan(text: ' ', style: _class),
+            TextSpan(
+              style: _title,
+              children: [TextSpan(text: 'MyClass', style: _title)],
+            ),
+            TextSpan(text: ' ', style: _class),
+          ],
         ),
-      ];
+        TextSpan(text: '{\n', style: _default),
+        TextSpan(text: '  ', style: _default.paled()),
+        TextSpan(
+          style: _function.paled(),
+          children: [
+            TextSpan(text: '', style: _function.paled()),
+            TextSpan(
+              style: _keyword.paled(),
+              children: [TextSpan(text: 'public', style: _keyword.paled())],
+            ),
+            TextSpan(text: ' ', style: _function.paled()),
+            TextSpan(
+              style: _keyword.paled(),
+              children: [TextSpan(text: 'void', style: _keyword.paled())],
+            ),
+            TextSpan(text: ' ', style: _function.paled()),
+            TextSpan(
+              style: _title.paled(),
+              children: [TextSpan(text: 'main', style: _title.paled())],
+            ),
+            TextSpan(text: '', style: _function.paled()),
+            TextSpan(
+              style: _params.paled(),
+              children: [TextSpan(text: '()', style: _params.paled())],
+            ),
+            TextSpan(text: ' ', style: _function.paled()),
+          ],
+        ),
+        TextSpan(text: '{ ', style: _default.paled()),
+        TextSpan(
+          style: _comment.paled(),
+          children: [TextSpan(text: '', style: _comment.paled())],
+        ),
+        TextSpan(text: '\n', style: _default.paled()),
+        TextSpan(text: '  }\n', style: _default),
+        TextSpan(text: '}\n', style: _default),
+        TextSpan(text: '', style: _default),
+      ],
+    ),
+  ),
 
-      for (final example in examples) {
+  'Contains newlines at leaf of node tree': _Example(
+    mode: dart,
+    text: '''
+void method1() 
+  if (false) {// [START section1]
+    return;
+  }// [END section1]
+}''',
+    readonlySectionNames: {'section1'},
+    expected: TextSpan(
+      style: _default,
+      children: [
+        TextSpan(text: '', style: _default),
+        TextSpan(
+          style: _keyword,
+          children: [TextSpan(text: 'void', style: _keyword)],
+        ),
+        TextSpan(text: ' method1() \n', style: _default),
+        TextSpan(text: '  ', style: _default.paled()),
+        TextSpan(
+          style: _keyword.paled(),
+          children: [TextSpan(text: 'if', style: _keyword.paled())],
+        ),
+        TextSpan(text: ' (', style: _default.paled()),
+        TextSpan(
+          style: _keyword.paled(),
+          children: [TextSpan(text: 'false', style: _keyword.paled())],
+        ),
+        TextSpan(text: ') {', style: _default.paled()),
+        TextSpan(
+          style: _comment.paled(),
+          children: [TextSpan(text: '', style: _comment.paled())],
+        ),
+        TextSpan(text: '\n', style: _default.paled()),
+        TextSpan(text: '    ', style: _default.paled()),
+        TextSpan(
+          style: _keyword.paled(),
+          children: [TextSpan(text: 'return', style: _keyword.paled())],
+        ),
+        TextSpan(text: ';\n', style: _default.paled()),
+        TextSpan(text: '  }', style: _default.paled()),
+        TextSpan(
+          style: _comment.paled(),
+          children: [TextSpan(text: '', style: _comment.paled())],
+        ),
+        TextSpan(text: '\n', style: _default.paled()),
+        TextSpan(text: '}', style: _default),
+      ],
+    ),
+  ),
+};
+
+void main() {
+  group('SpanBuilder. Builds highlighted. ', () {
+    _examples.forEach((name, example) {
+      test(name, () {
         Result? highlighted;
 
         final mode = example.mode;
@@ -190,31 +244,40 @@ public class MyClass {
           text: example.text,
           highlighted: highlighted,
           language: mode,
+          namedSectionParser: BracketsStartEndNamedSectionParser(),
+          readOnlySectionNames: example.readonlySectionNames,
         );
 
-        final builder = SpanBuilder(code: code, theme: _themeData);
+        final builder = SpanBuilder(
+          code: code,
+          theme: _themeData,
+          rootStyle: _default,
+        );
         final result = builder.build();
 
+        //Uncomment to see result in the console.
+        //print(result.toStringRecursive());
+
         expect(
           result,
           example.expected,
-          reason: example.name,
+          reason: name,
         );
-      }
+      });
     });
   });
 }
 
 class _Example {
-  final String name;
   final String text;
   final Mode? mode;
   final TextSpan expected;
+  final Set<String> readonlySectionNames;
 
-  _Example(
-    this.name, {
+  _Example({
     required this.text,
     required this.mode,
     required this.expected,
+    this.readonlySectionNames = const {},
   });
 }