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 8d2d166a..35e3bf00 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'; @@ -136,6 +137,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, @@ -150,11 +163,7 @@ class CodeController extends TextEditingController { this.readOnly = false, this.stringMap, 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); + }); +}