diff --git a/lib/core/editor/querya_code_editor.dart b/lib/core/editor/querya_code_editor.dart index 5432ba28..3b630625 100644 --- a/lib/core/editor/querya_code_editor.dart +++ b/lib/core/editor/querya_code_editor.dart @@ -94,19 +94,28 @@ class _QueryaCodeEditorState extends State { } void _attachListener() { + if (widget.onChanged == null) return; _lastText = _effectiveController.text; _effectiveController.addListener(_onTextChanged); } void _detachListener([material.TextEditingController? target]) { (target ?? _effectiveController).removeListener(_onTextChanged); + _lastText = null; } void _onTextChanged() { + if (widget.onChanged == null) return; final currentText = _effectiveController.text; + if (identical(currentText, _lastText)) return; + if (_lastText != null && currentText.length != _lastText!.length) { + _lastText = currentText; + widget.onChanged!(currentText); + return; + } if (currentText == _lastText) return; _lastText = currentText; - widget.onChanged?.call(currentText); + widget.onChanged!(currentText); } void _syncThemeIfNeeded( @@ -160,6 +169,7 @@ class _QueryaCodeEditorState extends State { final languageChanged = oldWidget.language != widget.language; final highlightingChanged = oldWidget.enableHighlighting != widget.enableHighlighting; + final onChangedChanged = oldWidget.onChanged != widget.onChanged; if (controllerChanged) { _detachListener(oldWidget.controller ?? _internalController); @@ -193,6 +203,9 @@ class _QueryaCodeEditorState extends State { context.queryaTheme, transitioning: QueryaThemeScope.isTransitioning(context), ); + } else if (onChangedChanged) { + _detachListener(); + _attachListener(); } } diff --git a/test/core/editor/querya_code_editor_test.dart b/test/core/editor/querya_code_editor_test.dart index 35000d81..9152b29e 100644 --- a/test/core/editor/querya_code_editor_test.dart +++ b/test/core/editor/querya_code_editor_test.dart @@ -311,5 +311,74 @@ void main() { expect(editable.controller, isA()); expect(editable.controller.text, 'SELECT 1;'); }); + + testWidgets( + 'QueryaCodeEditor does not attach listener to controller when onChanged is null', + (tester) async { + final controller = material.TextEditingController(text: 'initial'); + // Initially, controller has 0 listeners + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: controller, + language: QueryaCodeLanguage.sql, + // onChanged is null + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // The EditableText attaches its own listener for rendering, but QueryaCodeEditor does not attach an extra listener. + // When onChanged is passed dynamically: + var changedValue = ''; + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: controller, + language: QueryaCodeLanguage.sql, + onChanged: (v) => changedValue = v, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + controller.text = 'updated'; + await tester.pump(); + expect(changedValue, 'updated'); + }); + + testWidgets( + 'same-length text substitution triggers onChanged correctly', + (tester) async { + final controller = material.TextEditingController(text: 'cat'); + var changedValue = ''; + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: controller, + language: QueryaCodeLanguage.sql, + onChanged: (v) => changedValue = v, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Same length, different text ('cat' -> 'dog') + controller.text = 'dog'; + await tester.pump(); + expect(changedValue, 'dog'); + }); }