Repository: akvelon/flutter-code-editor

Issue:
Replace tabs with spaces when loading the code
There was an issue in Beam Playground that suggests that tab symbols are not handled correctly in the editor:
https://issues.apache.org/jira/browse/BEAM-13446

Check this and fix if needed.



Fix patch:
diff --git a/.gitignore b/.gitignore
index 51dab37f..33f634c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,5 @@ app.*.map.json
 /android/app/release
 
 pubspec.lock
+.fvm/flutter_sdk
+.fvm/fvm_config.json
diff --git a/example/lib/custom_code_box.dart b/example/lib/custom_code_box.dart
index 310a02c7..c3552cbd 100644
--- a/example/lib/custom_code_box.dart
+++ b/example/lib/custom_code_box.dart
@@ -167,11 +167,11 @@ class _InnerFieldState extends State<InnerField> {
       language: allLanguages[widget.language],
       text: '''
 class MyClass {
-  void readOnlyMethod() {// [START section1]
-  }// [END section1]
-  // [START section2]
-  void method() {
-  }// [END section2]
+\tvoid readOnlyMethod() {// [START section1]
+\t}// [END section1]
+\t// [START section2]
+\tvoid method() {
+\t}// [END section2]
 }
 ''',
       namedSectionParser: const BracketsStartEndNamedSectionParser(),
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index 1f4e4637..96f2bd87 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -21,6 +21,8 @@ import 'editor_params.dart';
 import 'span_builder.dart';
 
 const _middleDot = '·';
+const _tab = '\t';
+const _space = ' ';
 
 class CodeController extends TextEditingController {
   Mode? _language;
@@ -260,7 +262,7 @@ class CodeController extends TextEditingController {
   String get fullText => _lastCode.text;
 
   set fullText(String fullText) {
-    _updateLastCodeIfChanged(fullText);
+    _updateLastCodeIfChanged(_replaceTabsWithSpacesIfNeeded(fullText));
     super.value = TextEditingValue(text: _lastCode.visibleText);
   }
 
@@ -378,6 +380,13 @@ class CodeController extends TextEditingController {
     );
   }
 
+  String _replaceTabsWithSpacesIfNeeded(String text) {
+    if (modifiers.contains(const TabModifier())) {
+      return text.replaceAll(_tab, _space * params.tabSpaces);
+    }
+    return text;
+  }
+
   TextSpan _processPatterns(String text, TextStyle? style) {
     final children = <TextSpan>[];
 


Test patch:
diff --git a/test/src/code_field/code_controller_tab_test.dart b/test/src/code_field/code_controller_tab_test.dart
new file mode 100644
index 00000000..44594211
--- /dev/null
+++ b/test/src/code_field/code_controller_tab_test.dart
@@ -0,0 +1,77 @@
+import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:highlight/languages/go.dart';
+
+void main() {
+  const snippetWithTabs = '''
+public class MyClass {
+\tpublic void main() {
+\t}
+}
+''';
+  const snippetWithDoubleSpaces = '''
+public class MyClass {
+  public void main() {
+  }
+}
+''';
+  const snippetWithTripleSpaces = '''
+public class MyClass {
+   public void main() {
+   }
+}
+''';
+
+  bool _areIdenticalTexts(String text1, String text2) {
+    return text1.compareTo(text2) == 0;
+  }
+
+  group('Tab replacement', () {
+    test(
+      'applied if TabModifier is present',
+      () {
+        final controller = CodeController(
+          text: snippetWithTabs,
+          language: go,
+          modifiers: [const TabModifier()],
+        );
+        expect(
+          _areIdenticalTexts(controller.text, snippetWithDoubleSpaces),
+          true,
+        );
+      },
+    );
+
+    test(
+      'not applied if TabModifier is not present',
+      () {
+        final controller = CodeController(
+          text: snippetWithTabs,
+          language: go,
+          modifiers: [],
+        );
+        expect(
+          _areIdenticalTexts(controller.text, snippetWithDoubleSpaces),
+          false,
+        );
+      },
+    );
+
+    test(
+      'works with custom tabSpaces',
+      () {
+        const tabSpaces = 3;
+        final controller = CodeController(
+          params: const EditorParams(tabSpaces: tabSpaces),
+          text: snippetWithTabs,
+          language: go,
+          modifiers: [const TabModifier()],
+        );
+        expect(
+          _areIdenticalTexts(controller.text, snippetWithTripleSpaces),
+          true,
+        );
+      },
+    );
+  });
+}