diff --git a/lib/core/editor/querya_code_editor.dart b/lib/core/editor/querya_code_editor.dart index 6035aeb3..057b58e8 100644 --- a/lib/core/editor/querya_code_editor.dart +++ b/lib/core/editor/querya_code_editor.dart @@ -126,12 +126,12 @@ class _QueryaCodeEditorState extends State { _syncing = false; } - void _disposeHighlight() { + void _disposeHighlight([material.TextEditingController? externalToDetach]) { final highlight = _highlightController; if (highlight == null) return; highlight.removeListener(_onTextChanged); highlight.removeListener(_syncToExternal); - widget.controller?.removeListener(_syncFromExternal); + (externalToDetach ?? widget.controller)?.removeListener(_syncFromExternal); if (_ownsHighlightController) { highlight.dispose(); } @@ -230,7 +230,7 @@ class _QueryaCodeEditorState extends State { if (oldWidget.controller != widget.controller) { _highlightController?.removeListener(_onTextChanged); _plainController?.removeListener(_onTextChanged); - _disposeHighlight(); + _disposeHighlight(oldWidget.controller); if (_plainController != null) { _plainController!.removeListener(_onTextChanged); if (_ownsPlainController) { @@ -240,7 +240,6 @@ class _QueryaCodeEditorState extends State { } if (_useHighlighting) { _ensureHighlightController(context.queryaTheme); - _highlightController!.addListener(_onTextChanged); } else { _initPlainController(widget.controller); _plainController!.addListener(_onTextChanged); diff --git a/test/core/editor/querya_code_editor_test.dart b/test/core/editor/querya_code_editor_test.dart index 2eac736e..3d78133f 100644 --- a/test/core/editor/querya_code_editor_test.dart +++ b/test/core/editor/querya_code_editor_test.dart @@ -114,4 +114,58 @@ void main() { await tester.enterText(find.byType(material.EditableText), 'hello'); expect(last, 'hello'); }); + + testWidgets( + 'updating controller does not register duplicate listener on highlight controller', + (tester) async { + final controller1 = material.TextEditingController(text: 'SELECT 1;'); + final controller2 = material.TextEditingController(text: 'SELECT 2;'); + int changeCount = 0; + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: controller1, + language: QueryaCodeLanguage.sql, + onChanged: (_) => changeCount++, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await pumpSyntaxHighlightDebounce(tester); + + // Update widget with new controller reference + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox( + width: 400, + height: 200, + child: QueryaCodeEditor( + controller: controller2, + language: QueryaCodeLanguage.sql, + onChanged: (_) => changeCount++, + ), + ), + ), + ); + await tester.pumpAndSettle(); + await pumpSyntaxHighlightDebounce(tester); + + changeCount = 0; + controller2.text = 'SELECT 3;'; + await tester.pump(); + + // Verify onChanged fired exactly once (no duplicate listener registered) + expect(changeCount, 1); + + // Verify old controller was detached + controller1.text = 'SELECT 999;'; + await tester.pump(); + expect(changeCount, 1); + }); } +