From b0e893b04ad9e0b869ec627e5bf0f42bab382577 Mon Sep 17 00:00:00 2001 From: malarg Date: Tue, 25 Apr 2023 09:12:19 +0400 Subject: [PATCH 1/2] Issues: #192 Paired Symbols, #199 YAML support (Dev: @malarg) --- .../03.change_language_theme/constants.dart | 3 + lib/src/code_field/code_controller.dart | 19 ++- .../close_block_code_modifier.dart | 2 + lib/src/code_modifiers/insertion.dart | 47 ++++++ lib/src/folding/parsers/parser_factory.dart | 7 + lib/src/wip/autocomplete/popup.dart | 1 + .../wip/autocomplete/popup_controller.dart | 6 +- .../controller_insertion_test.dart | 149 ++++++++++++++++++ test/src/code_modifiers/insertion_test.dart | 75 +++++++++ 9 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 lib/src/code_modifiers/insertion.dart create mode 100644 test/src/code_modifiers/controller_insertion_test.dart create mode 100644 test/src/code_modifiers/insertion_test.dart 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 = [ @@ -21,6 +23,7 @@ const languageList = [ 'php', 'python', 'scala', + 'yaml', ]; const themeList = [ diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart index 5a4a5d92..63816140 100644 --- a/lib/src/code_field/code_controller.dart +++ b/lib/src/code_field/code_controller.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import '../../flutter_code_editor.dart'; import '../autocomplete/autocompleter.dart'; import '../code/code_edit_result.dart'; +import '../code_modifiers/insertion.dart'; import '../code/key_event.dart'; import '../history/code_history_controller.dart'; import '../history/code_history_record.dart'; @@ -132,6 +133,18 @@ class CodeController extends TextEditingController { EnterKeyIntent: EnterKeyAction(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 +156,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, diff --git a/lib/src/code_modifiers/close_block_code_modifier.dart b/lib/src/code_modifiers/close_block_code_modifier.dart index 7a719405..56699908 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/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/wip/autocomplete/popup.dart b/lib/src/wip/autocomplete/popup.dart index 90e28945..5035d8f0 100644 --- a/lib/src/wip/autocomplete/popup.dart +++ b/lib/src/wip/autocomplete/popup.dart @@ -47,6 +47,7 @@ class PopupState extends State { final pageStorageBucket = PageStorageBucket(); @override void initState() { + widget.controller.reset(); widget.controller.addListener(rebuild); super.initState(); } diff --git a/lib/src/wip/autocomplete/popup_controller.dart b/lib/src/wip/autocomplete/popup_controller.dart index bbbc3886..cbfc05d8 100644 --- a/lib/src/wip/autocomplete/popup_controller.dart +++ b/lib/src/wip/autocomplete/popup_controller.dart @@ -7,7 +7,7 @@ class PopupController extends ChangeNotifier { bool shouldShow = false; bool enabled = true; - final ItemScrollController itemScrollController = ItemScrollController(); + ItemScrollController itemScrollController = ItemScrollController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); @@ -23,6 +23,10 @@ class PopupController extends ChangeNotifier { int get selectedIndex => _selectedIndex; + void reset() { + itemScrollController = ItemScrollController(); + } + void show(List suggestions) { if (enabled == false) { return; 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..5b7b338c --- /dev/null +++ b/test/src/code_modifiers/insertion_test.dart @@ -0,0 +1,75 @@ +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() { + test('inserts at the start of string correctly', () { + const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); + const text = 'Hello World'; + final selection = TextSelection.fromPosition(const TextPosition(offset: 0)); + const editorParams = EditorParams(); + + 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 modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); + const text = 'Hello World'; + final selection = TextSelection.fromPosition(const TextPosition(offset: 5)); + const editorParams = EditorParams(); + + 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 modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); + const text = 'Hello World'; + final selection = + TextSelection.fromPosition(const TextPosition(offset: text.length)); + const editorParams = EditorParams(); + + 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 modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); + const text = 'Hello World'; + const selection = TextSelection( + baseOffset: 5, + extentOffset: 7, + ); + const editorParams = EditorParams(); + + 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 modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); + const text = ''; + final selection = TextSelection.fromPosition(const TextPosition(offset: 0)); + const editorParams = EditorParams(); + + final result = modifier.updateString(text, selection, editorParams); + + expect(result!.text, '123'); + expect(result.selection.baseOffset, 1); + expect(result.selection.extentOffset, 1); + }); +} From 36cd770c1741f9f3259f105a7e8c9958573bf7ae Mon Sep 17 00:00:00 2001 From: dzs4 Date: Thu, 6 Mar 2025 15:42:14 +0100 Subject: [PATCH 2/2] Minor improvements based on feedback at PR#231 --- lib/src/code_field/actions/tab.dart | 21 ++++ lib/src/code_field/code_controller.dart | 85 ++++++++++----- lib/src/code_field/code_field.dart | 6 ++ .../close_block_code_modifier.dart | 2 +- lib/src/code_modifiers/code_modifier.dart | 2 +- lib/src/util/string_util.dart | 14 +++ lib/src/wip/autocomplete/popup.dart | 11 +- .../wip/autocomplete/popup_controller.dart | 8 +- test/src/code_modifiers/insertion_test.dart | 100 +++++++++-------- test/src/search/code_controller_test.dart | 101 +++++++++++++++++- 10 files changed, 263 insertions(+), 87 deletions(-) create mode 100644 lib/src/code_field/actions/tab.dart create mode 100644 lib/src/util/string_util.dart 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 { + 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 63816140..a63ef50f 100644 --- a/lib/src/code_field/code_controller.dart +++ b/lib/src/code_field/code_controller.dart @@ -11,8 +11,8 @@ import 'package:meta/meta.dart'; import '../../flutter_code_editor.dart'; import '../autocomplete/autocompleter.dart'; import '../code/code_edit_result.dart'; -import '../code_modifiers/insertion.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'; @@ -20,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'; @@ -29,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'; @@ -51,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; @@ -108,6 +111,7 @@ class CodeController extends TextEditingController { SearchSettingsController get _searchSettingsController => searchController.settingsController; + SearchNavigationController get _searchNavigationController => searchController.navigationController; @@ -131,19 +135,20 @@ 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, - ]; + IndentModifier(), + CloseBlockModifier(), + TabModifier(), + InsertionCodeModifier.backticks, + InsertionCodeModifier.braces, + InsertionCodeModifier.brackets, + InsertionCodeModifier.doubleQuotes, + InsertionCodeModifier.parentheses, + InsertionCodeModifier.singleQuotes, + ]; CodeController({ String? text, @@ -356,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 = { 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 56699908..b40d22d0 100644 --- a/lib/src/code_modifiers/close_block_code_modifier.dart +++ b/lib/src/code_modifiers/close_block_code_modifier.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import '../code_field/editor_params.dart'; import 'code_modifier.dart'; -/// [CloseBlockModifier] is an implementation of [CodeModifier] +/// [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/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 5035d8f0..a102409d 100644 --- a/lib/src/wip/autocomplete/popup.dart +++ b/lib/src/wip/autocomplete/popup.dart @@ -45,9 +45,9 @@ class Popup extends StatefulWidget { class PopupState extends State { final pageStorageBucket = PageStorageBucket(); + @override void initState() { - widget.controller.reset(); widget.controller.addListener(rebuild); super.initState(); } @@ -69,6 +69,13 @@ class PopupState extends State { (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( @@ -98,7 +105,7 @@ class PopupState extends State { 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 cbfc05d8..de081b4d 100644 --- a/lib/src/wip/autocomplete/popup_controller.dart +++ b/lib/src/wip/autocomplete/popup_controller.dart @@ -7,7 +7,7 @@ class PopupController extends ChangeNotifier { bool shouldShow = false; bool enabled = true; - ItemScrollController itemScrollController = ItemScrollController(); + final ItemScrollController itemScrollController = ItemScrollController(); final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); @@ -23,12 +23,8 @@ class PopupController extends ChangeNotifier { int get selectedIndex => _selectedIndex; - void reset() { - itemScrollController = ItemScrollController(); - } - void show(List suggestions) { - if (enabled == false) { + if (!enabled) { return; } diff --git a/test/src/code_modifiers/insertion_test.dart b/test/src/code_modifiers/insertion_test.dart index 5b7b338c..7c63875b 100644 --- a/test/src/code_modifiers/insertion_test.dart +++ b/test/src/code_modifiers/insertion_test.dart @@ -4,72 +4,70 @@ import 'package:flutter_code_editor/src/code_modifiers/insertion.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('inserts at the start of string correctly', () { + group('InsertionCodeModifier', () { const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); - const text = 'Hello World'; - final selection = TextSelection.fromPosition(const TextPosition(offset: 0)); const editorParams = EditorParams(); - final result = modifier.updateString(text, selection, editorParams); + test('inserts at the start of string correctly', () { + const text = 'Hello World'; + final selection = + TextSelection.fromPosition(const TextPosition(offset: 0)); - expect(result!.text, '123Hello World'); - expect(result.selection.baseOffset, 1); - expect(result.selection.extentOffset, 1); - }); + final result = modifier.updateString(text, selection, editorParams); - test('inserts in the middle of string correctly', () { - const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); - const text = 'Hello World'; - final selection = TextSelection.fromPosition(const TextPosition(offset: 5)); - const editorParams = EditorParams(); + expect(result!.text, '123Hello World'); + expect(result.selection.baseOffset, 1); + expect(result.selection.extentOffset, 1); + }); - final result = modifier.updateString(text, selection, editorParams); + test('inserts in the middle of string correctly', () { + const text = 'Hello World'; + final selection = + TextSelection.fromPosition(const TextPosition(offset: 5)); - expect(result!.text, 'Hello123 World'); - expect(result.selection.baseOffset, 6); - expect(result.selection.extentOffset, 6); - }); + final result = modifier.updateString(text, selection, editorParams); - test('inserts at the end of string correctly', () { - const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); - const text = 'Hello World'; - final selection = - TextSelection.fromPosition(const TextPosition(offset: text.length)); - const editorParams = EditorParams(); + expect(result!.text, 'Hello123 World'); + expect(result.selection.baseOffset, 6); + expect(result.selection.extentOffset, 6); + }); - final result = modifier.updateString(text, selection, editorParams); + test('inserts at the end of string correctly', () { + const text = 'Hello World'; + final selection = + TextSelection.fromPosition(const TextPosition(offset: text.length)); - expect(result!.text, 'Hello World123'); - expect(result.selection.baseOffset, text.length + 1); - expect(result.selection.extentOffset, text.length + 1); - }); + final result = modifier.updateString(text, selection, editorParams); - test('inserts in the middle of string with selection correctly', () { - const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); - const text = 'Hello World'; - const selection = TextSelection( - baseOffset: 5, - extentOffset: 7, - ); - const editorParams = EditorParams(); + expect(result!.text, 'Hello World123'); + expect(result.selection.baseOffset, text.length + 1); + expect(result.selection.extentOffset, text.length + 1); + }); - final result = modifier.updateString(text, selection, editorParams); + test('inserts in the middle of string with selection correctly', () { + const text = 'Hello World'; + const selection = TextSelection( + baseOffset: 5, + extentOffset: 7, + ); - expect(result!.text, 'Hello123orld'); - expect(result.selection.baseOffset, 6); - expect(result.selection.extentOffset, 6); - }); + final result = modifier.updateString(text, selection, editorParams); - test('inserts at empty string correctly', () { - const modifier = InsertionCodeModifier(openChar: '1', closeString: '23'); - const text = ''; - final selection = TextSelection.fromPosition(const TextPosition(offset: 0)); - const editorParams = 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); + final result = modifier.updateString(text, selection, editorParams); - expect(result!.text, '123'); - expect(result.selection.baseOffset, 1); - expect(result.selection.extentOffset, 1); + 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 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: ']'); + }); + }); }