Repository: akvelon/flutter-code-editor (dart)

Issue:
Automatically insert pair character for brackets, square braces, angle brackets, parentheses, quotes

Pull request:
resolves #192
resolves #199 
resolves #235 

Origin: https://github.com/akvelon/flutter-code-editor/pull/231, original author of those fixes: @Malarg 

Also, fixed an issue when the selection after inserting a keyword locates in an inappropriate place.
Also, added support of the Tab key for inserting the word

Patch:
diff --git a/example/lib/03.change_language_theme/constants.dart b/example/lib/03.change_language_theme/constants.dart
index 9b0379f3..ff091ca5 100644
--- a/example/lib/03.change_language_theme/constants.dart
+++ b/example/lib/03.change_language_theme/constants.dart
@@ -4,6 +4,7 @@ import 'package:highlight/languages/java.dart';
 import 'package:highlight/languages/php.dart';
 import 'package:highlight/languages/python.dart';
 import 'package:highlight/languages/scala.dart';
+import 'package:highlight/languages/yaml.dart';
 
 final builtinLanguages = {
   'dart': dart,
@@ -12,6 +13,7 @@ final builtinLanguages = {
   'php': php,
   'python': python,
   'scala': scala,
+  'yaml': yaml,
 };
 
 const languageList = <String>[
@@ -21,6 +23,7 @@ const languageList = <String>[
   'php',
   'python',
   'scala',
+  'yaml',
 ];
 
 const themeList = <String>[
diff --git a/lib/src/code_field/actions/tab.dart b/lib/src/code_field/actions/tab.dart
new file mode 100644
index 00000000..aa991d07
--- /dev/null
+++ b/lib/src/code_field/actions/tab.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+
+import '../code_controller.dart';
+
+class TabKeyIntent extends Intent {
+  const TabKeyIntent();
+}
+
+class TabKeyAction extends Action<TabKeyIntent> {
+  final CodeController controller;
+
+  TabKeyAction({
+    required this.controller,
+  });
+
+  @override
+  Object? invoke(TabKeyIntent intent) {
+    controller.onTabKeyAction();
+    return null;
+  }
+}
diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart
index 5a4a5d92..a63ef50f 100644
--- a/lib/src/code_field/code_controller.dart
+++ b/lib/src/code_field/code_controller.dart
@@ -12,6 +12,7 @@ import '../../flutter_code_editor.dart';
 import '../autocomplete/autocompleter.dart';
 import '../code/code_edit_result.dart';
 import '../code/key_event.dart';
+import '../code_modifiers/insertion.dart';
 import '../history/code_history_controller.dart';
 import '../history/code_history_record.dart';
 import '../search/controller.dart';
@@ -19,6 +20,7 @@ import '../search/result.dart';
 import '../search/search_navigation_controller.dart';
 import '../search/settings_controller.dart';
 import '../single_line_comments/parser/single_line_comments.dart';
+import '../util/string_util.dart';
 import '../wip/autocomplete/popup_controller.dart';
 import 'actions/comment_uncomment.dart';
 import 'actions/copy.dart';
@@ -28,6 +30,7 @@ import 'actions/indent.dart';
 import 'actions/outdent.dart';
 import 'actions/redo.dart';
 import 'actions/search.dart';
+import 'actions/tab.dart';
 import 'actions/undo.dart';
 import 'search_result_highlighted_builder.dart';
 import 'span_builder.dart';
@@ -50,6 +53,7 @@ class CodeController extends TextEditingController {
   /// Calls [AbstractAnalyzer.analyze] after change with 500ms debounce.
   AbstractAnalyzer get analyzer => _analyzer;
   AbstractAnalyzer _analyzer;
+
   set analyzer(AbstractAnalyzer analyzer) {
     if (_analyzer == analyzer) {
       return;
@@ -107,6 +111,7 @@ class CodeController extends TextEditingController {
 
   SearchSettingsController get _searchSettingsController =>
       searchController.settingsController;
+
   SearchNavigationController get _searchNavigationController =>
       searchController.navigationController;
 
@@ -130,8 +135,21 @@ class CodeController extends TextEditingController {
     SearchIntent: SearchAction(controller: this),
     DismissIntent: CustomDismissAction(controller: this),
     EnterKeyIntent: EnterKeyAction(controller: this),
+    TabKeyIntent: TabKeyAction(controller: this),
   };
 
+  static const defaultCodeModifiers = [
+    IndentModifier(),
+    CloseBlockModifier(),
+    TabModifier(),
+    InsertionCodeModifier.backticks,
+    InsertionCodeModifier.braces,
+    InsertionCodeModifier.brackets,
+    InsertionCodeModifier.doubleQuotes,
+    InsertionCodeModifier.parentheses,
+    InsertionCodeModifier.singleQuotes,
+  ];
+
   CodeController({
     String? text,
     Mode? language,
@@ -143,11 +161,7 @@ class CodeController extends TextEditingController {
     this.patternMap,
     this.readOnly = false,
     this.params = const EditorParams(),
-    this.modifiers = const [
-      IndentModifier(),
-      CloseBlockModifier(),
-      TabModifier(),
-    ],
+    this.modifiers = defaultCodeModifiers,
   })  : _analyzer = analyzer,
         _readOnlySectionNames = readOnlySectionNames,
         _code = Code.empty,
@@ -347,30 +361,60 @@ class CodeController extends TextEditingController {
     insertStr('\n');
   }
 
+  void onTabKeyAction() {
+    if (popupController.shouldShow) {
+      insertSelectedWord();
+      return;
+    }
+
+    insertStr(' ' * params.tabSpaces);
+  }
+
   /// Inserts the word selected from the list of completions
   void insertSelectedWord() {
     final previousSelection = selection;
     final selectedWord = popupController.getSelectedWord();
     final startPosition = value.wordAtCursorStart;
+    final currentWord = value.wordAtCursor;
 
-    if (startPosition != null) {
-      final replacedText = text.replaceRange(
-        startPosition,
-        selection.baseOffset,
-        selectedWord,
-      );
+    if (startPosition == null || currentWord == null) {
+      popupController.hide();
+      return;
+    }
 
-      final adjustedSelection = previousSelection.copyWith(
-        baseOffset: startPosition + selectedWord.length,
-        extentOffset: startPosition + selectedWord.length,
-      );
+    final endReplacingPosition = startPosition + currentWord.length;
+    final endSelectionPosition = startPosition + selectedWord.length;
 
-      value = TextEditingValue(
-        text: replacedText,
-        selection: adjustedSelection,
-      );
+    var additionalSpaceIfEnd = '';
+    var offsetIfEndsWithSpace = 1;
+    if (text.length < endReplacingPosition + 1) {
+      additionalSpaceIfEnd = ' ';
+    } else {
+      final charAfterText = text[endReplacingPosition];
+      if (charAfterText != ' ' &&
+          !StringUtil.isDigit(charAfterText) &&
+          !StringUtil.isLetterEng(charAfterText)) {
+        // ex. case ';' or other finalizer, or symbol
+        offsetIfEndsWithSpace = 0;
+      }
     }
 
+    final replacedText = text.replaceRange(
+      startPosition,
+      endReplacingPosition,
+      '$selectedWord$additionalSpaceIfEnd',
+    );
+
+    final adjustedSelection = previousSelection.copyWith(
+      baseOffset: endSelectionPosition + offsetIfEndsWithSpace,
+      extentOffset: endSelectionPosition + offsetIfEndsWithSpace,
+    );
+
+    value = TextEditingValue(
+      text: replacedText,
+      selection: adjustedSelection,
+    );
+
     popupController.hide();
   }
 
diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart
index 5ea622d9..03a3d876 100644
--- a/lib/src/code_field/code_field.dart
+++ b/lib/src/code_field/code_field.dart
@@ -15,6 +15,7 @@ import 'actions/enter_key.dart';
 import 'actions/indent.dart';
 import 'actions/outdent.dart';
 import 'actions/search.dart';
+import 'actions/tab.dart';
 import 'code_controller.dart';
 import 'default_styles.dart';
 import 'js_workarounds/js_workarounds.dart';
@@ -110,6 +111,11 @@ final _shortcuts = <ShortcutActivator, Intent>{
   LogicalKeySet(
     LogicalKeyboardKey.enter,
   ): const EnterKeyIntent(),
+
+  // TabKey
+  LogicalKeySet(
+    LogicalKeyboardKey.tab,
+  ): const TabKeyIntent(),
 };
 
 class CodeField extends StatefulWidget {
diff --git a/lib/src/code_modifiers/close_block_code_modifier.dart b/lib/src/code_modifiers/close_block_code_modifier.dart
index 7a719405..b40d22d0 100644
--- a/lib/src/code_modifiers/close_block_code_modifier.dart
+++ b/lib/src/code_modifiers/close_block_code_modifier.dart
@@ -5,6 +5,8 @@ import 'package:flutter/widgets.dart';
 import '../code_field/editor_params.dart';
 import 'code_modifier.dart';
 
+/// [CloseBlockModifier] is an implementation of [CodeModifier]
+/// that remove spaces before the closing bracket, if required.
 class CloseBlockModifier extends CodeModifier {
   const CloseBlockModifier() : super('}');
 
diff --git a/lib/src/code_modifiers/code_modifier.dart b/lib/src/code_modifiers/code_modifier.dart
index b2a4d53f..c6813851 100644
--- a/lib/src/code_modifiers/code_modifier.dart
+++ b/lib/src/code_modifiers/code_modifier.dart
@@ -7,7 +7,7 @@ abstract class CodeModifier {
 
   const CodeModifier(this.char);
 
-  // Helper to insert [str] in [text] between [start] and [end]
+  /// Helper to insert [str] in [text] between [start] and [end]
   TextEditingValue replace(String text, int start, int end, String str) {
     final len = str.length;
     return TextEditingValue(
diff --git a/lib/src/code_modifiers/insertion.dart b/lib/src/code_modifiers/insertion.dart
new file mode 100644
index 00000000..ef967b9e
--- /dev/null
+++ b/lib/src/code_modifiers/insertion.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/services.dart';
+
+import '../code_field/editor_params.dart';
+import 'code_modifier.dart';
+
+class InsertionCodeModifier extends CodeModifier {
+  final String openChar;
+  final String closeString;
+
+  const InsertionCodeModifier({
+    required this.openChar,
+    required this.closeString,
+  }) : super(openChar);
+
+  static const backticks =
+      InsertionCodeModifier(openChar: '`', closeString: '`');
+
+  static const braces = InsertionCodeModifier(openChar: '{', closeString: '}');
+
+  static const brackets =
+      InsertionCodeModifier(openChar: '[', closeString: ']');
+
+  static const doubleQuotes =
+      InsertionCodeModifier(openChar: '"', closeString: '"');
+
+  static const parentheses =
+      InsertionCodeModifier(openChar: '(', closeString: ')');
+
+  static const singleQuotes =
+      InsertionCodeModifier(openChar: '\'', closeString: '\'');
+
+  @override
+  TextEditingValue? updateString(
+    String text,
+    TextSelection sel,
+    EditorParams params,
+  ) {
+    final replaced = replace(text, sel.start, sel.end, '$openChar$closeString');
+
+    return replaced.copyWith(
+      selection: TextSelection(
+        baseOffset: replaced.selection.baseOffset - closeString.length,
+        extentOffset: replaced.selection.extentOffset - closeString.length,
+      ),
+    );
+  }
+}
diff --git a/lib/src/folding/parsers/parser_factory.dart b/lib/src/folding/parsers/parser_factory.dart
index 6174775e..586efcea 100644
--- a/lib/src/folding/parsers/parser_factory.dart
+++ b/lib/src/folding/parsers/parser_factory.dart
@@ -1,9 +1,11 @@
 import 'package:highlight/highlight_core.dart';
 import 'package:highlight/languages/java.dart';
 import 'package:highlight/languages/python.dart';
+import 'package:highlight/languages/yaml.dart';
 
 import 'abstract.dart';
 import 'highlight.dart';
+import 'indent.dart';
 import 'java.dart';
 import 'python.dart';
 
@@ -15,6 +17,11 @@ class FoldableBlockParserFactory {
     if (mode == java) {
       return JavaFoldableBlockParser();
     }
+
+    if (mode == yaml) {
+      return IndentFoldableBlockParser();
+    }
+
     return HighlightFoldableBlockParser();
   }
 }
diff --git a/lib/src/util/string_util.dart b/lib/src/util/string_util.dart
new file mode 100644
index 00000000..63212cf0
--- /dev/null
+++ b/lib/src/util/string_util.dart
@@ -0,0 +1,14 @@
+class StringUtil {
+  static bool isDigit(String char) {
+    if (char.isEmpty || char.length > 1) return false;
+    final codeUnit = char.codeUnitAt(0);
+    return codeUnit >= 48 && codeUnit <= 57; // Unicode range for '0' to '9'
+  }
+
+  static bool isLetterEng(String char) {
+    if (char.isEmpty || char.length > 1) return false;
+    final codeUnit = char.codeUnitAt(0);
+    return (codeUnit >= 65 && codeUnit <= 90) || // A-Z
+        (codeUnit >= 97 && codeUnit <= 122); // a-z
+  }
+}
diff --git a/lib/src/wip/autocomplete/popup.dart b/lib/src/wip/autocomplete/popup.dart
index 90e28945..a102409d 100644
--- a/lib/src/wip/autocomplete/popup.dart
+++ b/lib/src/wip/autocomplete/popup.dart
@@ -45,6 +45,7 @@ class Popup extends StatefulWidget {
 
 class PopupState extends State<Popup> {
   final pageStorageBucket = PageStorageBucket();
+
   @override
   void initState() {
     widget.controller.addListener(rebuild);
@@ -68,6 +69,13 @@ class PopupState extends State<Popup> {
             (widget.editorOffset?.dx ?? 0) -
             100;
 
+    // Fixes assertion error when ISC isn't attached but _attach method
+    // of ISC instance are being called
+    ItemScrollController? isc;
+    if (widget.controller.itemScrollController.isAttached) {
+      isc = widget.controller.itemScrollController;
+    }
+
     return PageStorage(
       bucket: pageStorageBucket,
       child: Positioned(
@@ -97,7 +105,7 @@ class PopupState extends State<Popup> {
             child: ScrollablePositionedList.builder(
               shrinkWrap: true,
               physics: const ClampingScrollPhysics(),
-              itemScrollController: widget.controller.itemScrollController,
+              itemScrollController: isc,
               itemPositionsListener: widget.controller.itemPositionsListener,
               itemCount: widget.controller.suggestions.length,
               itemBuilder: (context, index) {
diff --git a/lib/src/wip/autocomplete/popup_controller.dart b/lib/src/wip/autocomplete/popup_controller.dart
index bbbc3886..de081b4d 100644
--- a/lib/src/wip/autocomplete/popup_controller.dart
+++ b/lib/src/wip/autocomplete/popup_controller.dart
@@ -24,7 +24,7 @@ class PopupController extends ChangeNotifier {
   int get selectedIndex => _selectedIndex;
 
   void show(List<String> suggestions) {
-    if (enabled == false) {
+    if (!enabled) {
       return;
     }
 


Test patch:
diff --git a/test/src/code_modifiers/controller_insertion_test.dart b/test/src/code_modifiers/controller_insertion_test.dart
new file mode 100644
index 00000000..9019e708
--- /dev/null
+++ b/test/src/code_modifiers/controller_insertion_test.dart
@@ -0,0 +1,149 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_code_editor/flutter_code_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('Insertion modifier test', () {
+    const examples = [
+      //
+      _Example(
+        'Add backticks',
+        initialValue: TextEditingValue(
+          text: 'dict',
+          //     \ cursor
+          selection: TextSelection.collapsed(offset: 0),
+        ),
+        expected: TextEditingValue(
+          text: '``dict',
+          //      \ cursor
+          selection: TextSelection.collapsed(offset: 1),
+        ),
+        inputChar: '`',
+      ),
+
+      _Example(
+        'Add char at the start of the string (braces)',
+        initialValue: TextEditingValue(
+          text: 'dict',
+          //     \ cursor
+          selection: TextSelection.collapsed(offset: 0),
+        ),
+        expected: TextEditingValue(
+          text: '{}dict',
+          //      \ cursor
+          selection: TextSelection.collapsed(offset: 1),
+        ),
+        inputChar: '{',
+      ),
+
+      _Example(
+        'Add char in the middle of the string (parentheses)',
+        initialValue: TextEditingValue(
+          text: 'print',
+          //        \ cursor
+          selection: TextSelection.collapsed(offset: 3),
+        ),
+        expected: TextEditingValue(
+          text: 'pri()nt',
+          //         \ cursor
+          selection: TextSelection.collapsed(offset: 4),
+        ),
+        inputChar: '(',
+      ),
+
+      _Example(
+        'Add char at the end of the string (brackets)',
+        initialValue: TextEditingValue(
+          text: 'print',
+          //          \ cursor
+          selection: TextSelection.collapsed(offset: 5),
+        ),
+        expected: TextEditingValue(
+          text: 'print[]',
+          //           \ cursor
+          selection: TextSelection.collapsed(offset: 6),
+        ),
+        inputChar: '[',
+      ),
+
+      _Example(
+        'Add close char before same close char (double quotes)',
+        initialValue: TextEditingValue(
+          text: 'string"',
+          //           \ cursor
+          selection: TextSelection.collapsed(offset: 6),
+        ),
+        expected: TextEditingValue(
+          text: 'string"""',
+          //            \ cursor
+          selection: TextSelection.collapsed(offset: 7),
+        ),
+        inputChar: '"',
+      ),
+
+      _Example(
+        'Empty initial string (single quotes)',
+        initialValue: TextEditingValue(
+          // ignore: avoid_redundant_argument_values
+          text: '',
+          //     \ cursor
+          selection: TextSelection.collapsed(offset: 0),
+        ),
+        expected: TextEditingValue(
+          text: '\'\'',
+          //       \ cursor
+          selection: TextSelection.collapsed(offset: 1),
+        ),
+        inputChar: '\'',
+      ),
+    ];
+
+    for (final example in examples) {
+      final controller = CodeController();
+      controller.value = example.initialValue;
+      controller.value = _addCharToSelectedPosition(
+        controller.value,
+        example.inputChar,
+      );
+
+      expect(
+        controller.value,
+        example.expected,
+        reason: example.name,
+      );
+    }
+  });
+}
+
+TextEditingValue _addCharToSelectedPosition(
+  TextEditingValue value,
+  String char,
+) {
+  final selection = value.selection;
+  final text = value.text;
+
+  final newText = text.substring(0, selection.start) +
+      char +
+      text.substring(selection.start);
+
+  return TextEditingValue(
+    text: newText,
+    selection: TextSelection.collapsed(
+      offset: selection.start + char.length,
+    ),
+  );
+}
+
+class _Example {
+  final String name;
+  final TextEditingValue initialValue;
+  final TextEditingValue expected;
+  final String inputChar;
+
+  const _Example(
+    this.name, {
+    required this.initialValue,
+    required this.expected,
+    required this.inputChar,
+  });
+}
diff --git a/test/src/code_modifiers/insertion_test.dart b/test/src/code_modifiers/insertion_test.dart
new file mode 100644
index 00000000..7c63875b
--- /dev/null
+++ b/test/src/code_modifiers/insertion_test.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_code_editor/src/code_field/editor_params.dart';
+import 'package:flutter_code_editor/src/code_modifiers/insertion.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('InsertionCodeModifier', () {
+    const modifier = InsertionCodeModifier(openChar: '1', closeString: '23');
+    const editorParams = EditorParams();
+
+    test('inserts at the start of string correctly', () {
+      const text = 'Hello World';
+      final selection =
+          TextSelection.fromPosition(const TextPosition(offset: 0));
+
+      final result = modifier.updateString(text, selection, editorParams);
+
+      expect(result!.text, '123Hello World');
+      expect(result.selection.baseOffset, 1);
+      expect(result.selection.extentOffset, 1);
+    });
+
+    test('inserts in the middle of string correctly', () {
+      const text = 'Hello World';
+      final selection =
+          TextSelection.fromPosition(const TextPosition(offset: 5));
+
+      final result = modifier.updateString(text, selection, editorParams);
+
+      expect(result!.text, 'Hello123 World');
+      expect(result.selection.baseOffset, 6);
+      expect(result.selection.extentOffset, 6);
+    });
+
+    test('inserts at the end of string correctly', () {
+      const text = 'Hello World';
+      final selection =
+          TextSelection.fromPosition(const TextPosition(offset: text.length));
+
+      final result = modifier.updateString(text, selection, editorParams);
+
+      expect(result!.text, 'Hello World123');
+      expect(result.selection.baseOffset, text.length + 1);
+      expect(result.selection.extentOffset, text.length + 1);
+    });
+
+    test('inserts in the middle of string with selection correctly', () {
+      const text = 'Hello World';
+      const selection = TextSelection(
+        baseOffset: 5,
+        extentOffset: 7,
+      );
+
+      final result = modifier.updateString(text, selection, editorParams);
+
+      expect(result!.text, 'Hello123orld');
+      expect(result.selection.baseOffset, 6);
+      expect(result.selection.extentOffset, 6);
+    });
+
+    test('inserts at empty string correctly', () {
+      const text = '';
+      final selection =
+          TextSelection.fromPosition(const TextPosition(offset: 0));
+
+      final result = modifier.updateString(text, selection, editorParams);
+
+      expect(result!.text, '123');
+      expect(result.selection.baseOffset, 1);
+      expect(result.selection.extentOffset, 1);
+    });
+  });
+}
diff --git a/test/src/search/code_controller_test.dart b/test/src/search/code_controller_test.dart
index cd3c96fc..35260bb9 100644
--- a/test/src/search/code_controller_test.dart
+++ b/test/src/search/code_controller_test.dart
@@ -1,4 +1,5 @@
 import 'package:flutter/services.dart';
+import 'package:flutter_code_editor/flutter_code_editor.dart';
 import 'package:flutter_code_editor/src/search/match.dart';
 import 'package:flutter_code_editor/src/search/result.dart';
 import 'package:flutter_code_editor/src/search/settings.dart';
@@ -7,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
 import '../common/create_app.dart';
 
 void main() {
-  group('CodeController', () {
+  group('CodeController, Search-related functionality', () {
     testWidgets('CTRL + F shows search, Escape hides', (wt) async {
       const text = 'AaAa';
       final controller = await pumpController(wt, text);
@@ -60,4 +61,102 @@ void main() {
       await wt.pumpAndSettle();
     });
   });
+
+  /// Requested to add at {@link https://github.com/akvelon/flutter-code-editor/pull/231}
+  group('CodeController-related formatting checks [Default params]', () {
+    /// tests insertion of existing modifiers into the code controller
+    /// at the defined index
+    void testInsertionAtIndex(
+      CodeController controller,
+      String initialText,
+      String insertedStart,
+      String insertedEnd,
+      int insertionIndex,
+    ) {
+      final selection = TextSelection(
+        baseOffset: insertionIndex,
+        extentOffset: insertionIndex,
+      );
+      // to move selection of textEditingValue at defined place
+      controller.value = TextEditingValue(
+        text: initialText,
+        selection: selection,
+      );
+
+      final textWithInsertedStart = initialText.replaceRange(
+        insertionIndex,
+        insertionIndex,
+        insertedStart,
+      );
+      controller.value = TextEditingValue(
+        text: textWithInsertedStart,
+        selection: selection,
+      );
+
+      final expectedText = initialText.replaceRange(
+        insertionIndex,
+        insertionIndex,
+        '$insertedStart$insertedEnd',
+      );
+      expect(controller.value.text, expectedText);
+    }
+
+    /// tests insertion at the start, middle and end of the initial text
+    Future<void> testInsertion(
+      WidgetTester wt,
+      String insertedStart, {
+      String? insertedEnd,
+    }) async {
+      insertedEnd ??= insertedStart;
+
+      const initialText = 'Hello';
+      final controller = await pumpController(wt, initialText);
+
+      testInsertionAtIndex(
+        controller,
+        initialText,
+        insertedStart,
+        insertedEnd,
+        0,
+      );
+      testInsertionAtIndex(
+        controller,
+        initialText,
+        insertedStart,
+        insertedEnd,
+        2,
+      );
+      testInsertionAtIndex(
+        controller,
+        initialText,
+        insertedStart,
+        insertedEnd,
+        initialText.length,
+      );
+    }
+
+    testWidgets('controller handles insertion of backticks', (wt) async {
+      await testInsertion(wt, '`');
+    });
+
+    testWidgets('controller handles insertion of single quotes', (wt) async {
+      await testInsertion(wt, '\'');
+    });
+
+    testWidgets('controller handles insertion of double quotes', (wt) async {
+      await testInsertion(wt, '"');
+    });
+
+    testWidgets('controller handles insertion of parentheses', (wt) async {
+      await testInsertion(wt, '(', insertedEnd: ')');
+    });
+
+    testWidgets('controller handles insertion of braces', (wt) async {
+      await testInsertion(wt, '{', insertedEnd: '}');
+    });
+
+    testWidgets('controller handles insertion of square brackets', (wt) async {
+      await testInsertion(wt, '[', insertedEnd: ']');
+    });
+  });
 }