diff --git a/lib/src/code_field/code_controller.dart b/lib/src/code_field/code_controller.dart index 12589bec..814e781d 100644 --- a/lib/src/code_field/code_controller.dart +++ b/lib/src/code_field/code_controller.dart @@ -846,6 +846,7 @@ class CodeController extends TextEditingController { @override void dispose() { _debounce?.cancel(); + historyController.dispose(); super.dispose(); } diff --git a/lib/src/history/code_history_controller.dart b/lib/src/history/code_history_controller.dart index 023832b5..56036dbc 100644 --- a/lib/src/history/code_history_controller.dart +++ b/lib/src/history/code_history_controller.dart @@ -36,9 +36,7 @@ class CodeHistoryController { CodeHistoryController({ required this.codeController, }) : lastCode = codeController.code, - lastSelection = codeController.value.selection { - _push(); - } + lastSelection = codeController.value.selection; void beforeChanged({ required Code code, @@ -50,19 +48,30 @@ class CodeHistoryController { } bool shouldSave = false; + bool isNewLineAdded = false; + bool onlySelectionChanged = false; - if (!shouldSave && _wasTextChanged) { + if (stack.isEmpty) { + shouldSave = true; + } + + if (_wasTextChanged) { // Inserting and deleting lines are significant enough // to save a record without waiting for idle. - shouldSave = code.lines.lines.length != lastCode.lines.lines.length; + if (code.lines.lines.length != lastCode.lines.lines.length) { + shouldSave = true; + isNewLineAdded = true; + } + } + + if (!isTextChanging && lastSelection != selection && selection.isValid) { + onlySelectionChanged = true; } - if (!shouldSave) { + if (!shouldSave && !onlySelectionChanged) { if (isTextChanging) { _wasTextChanged = true; - } - if (_wasTextChanged) { final isText1CharLonger = code.text.length == lastCode.text.length + 1; final isTypingContinuous = isText1CharLonger && selection.hasMovedOneCharacterRight(lastSelection); @@ -75,16 +84,26 @@ class CodeHistoryController { } } - if (shouldSave) { + final shouldAddRecordBefore = + _wasTextChanged && (onlySelectionChanged || isNewLineAdded); + + if (shouldAddRecordBefore) { _push(); } lastCode = code; lastSelection = selection; + + if (shouldSave) { + _push(); + } } void _dropRedoIfAny() { - stack.removeStartingAt(_currentRecordIndex + 1); + final startIndex = _currentRecordIndex + 1; + if (startIndex < stack.length) { + stack.removeStartingAt(startIndex); + } } void undo() { @@ -116,6 +135,7 @@ class CodeHistoryController { void _push() { _debounceTimer?.cancel(); + _dropRedoIfAny(); _pushRecord(_createRecord()); _wasTextChanged = false; } @@ -142,4 +162,8 @@ class CodeHistoryController { _push(); _currentRecordIndex = 0; } + + void dispose() { + _debounceTimer?.cancel(); + } } diff --git a/test/src/code_field/code_controller_folding_editing_test.dart b/test/src/code_field/code_controller_folding_editing_test.dart index fba90bce..42a4d0a1 100644 --- a/test/src/code_field/code_controller_folding_editing_test.dart +++ b/test/src/code_field/code_controller_folding_editing_test.dart @@ -237,7 +237,6 @@ int n; expect(controller.value.text, visible + inserted); expect(controller.code.foldedBlocks.length, 1); - await wt.moveCursor(-1); // Reset timer. }); testWidgets('Add comment after folded comments block -> Unfold', @@ -272,8 +271,6 @@ int n; controller.value = controller.value.replacedSelection(inserted); expect(controller.value.text, visible + inserted); expect(controller.code.foldedBlocks.length, 1); - - await wt.moveCursor(-1); // Reset timer. }); }); @@ -350,7 +347,6 @@ int n; expect(controller.value.text, expectedText); expect(controller.code.foldedBlocks.length, 1); - await wt.moveCursor(-1); // Reset timer. }); }); }); diff --git a/test/src/history/code_history_controller_test.dart b/test/src/history/code_history_controller_test.dart index a46f5308..76478c03 100644 --- a/test/src/history/code_history_controller_test.dart +++ b/test/src/history/code_history_controller_test.dart @@ -19,12 +19,7 @@ void main() { expect( controller.historyController.stack, - [ - CodeHistoryRecord( - code: controller.code, - selection: const TextSelection.collapsed(offset: -1), - ), - ], + [], ); }); @@ -34,6 +29,8 @@ void main() { final controller = await pumpController(wt, MethodSnippet.full); await wt.cursorEnd(); + expect(controller.historyController.stack.length, 1); + controller.value = controller.value; controller.value = controller.value.typed('a'); controller.value = controller.value.typed('b'); @@ -42,82 +39,70 @@ void main() { controller.unfoldAt(0); expect(controller.historyController.stack.length, 1); - await wt.moveCursor(-1); // Clear timer. }, ); - testWidgets('Selection change does not create', (WidgetTester wt) async { + testWidgets('Empty stack -> any change creates record', + (WidgetTester wt) async { final controller = await pumpController(wt, MethodSnippet.full); + expect(controller.historyController.stack.length, 0); await wt.moveCursor(-1); - await wt.moveCursor(1); + expect(controller.historyController.stack.length, 1); + }); + + testWidgets('Only selection change -> does not create', + (WidgetTester wt) async { + final controller = await pumpController(wt, MethodSnippet.full); + expect(controller.historyController.stack.length, 0); + await wt.moveCursor(-1); // Creates because empty stack + expect(controller.historyController.stack.length, 1); + + await wt.moveCursor(-5); expect(controller.historyController.stack.length, 1); }); testWidgets('Typing + Selection change', (WidgetTester wt) async { final controller = await pumpController(wt, MethodSnippet.full); - await wt.cursorEnd(); + expect(controller.historyController.stack.length, 0); + + await wt.cursorEnd(); // Creates because stack is empty. + + expect(controller.historyController.stack.length, 1); controller.value = controller.value.typed('a'); final code1 = controller.code; await wt.moveCursor(-1); // Creates. + expect(controller.historyController.stack.length, 2); controller.value = controller.value.typed('b'); final code2 = controller.code; await wt.moveCursor(1); // Creates. - expect(controller.historyController.stack.length, 3); - expect( - controller.historyController.stack[1], - CodeHistoryRecord( - code: code1, - selection: const TextSelection.collapsed( - offset: MethodSnippet.visible.length + 1, - ), - ), - ); - expect( - controller.historyController.stack[2], - CodeHistoryRecord( - code: code2, - selection: const TextSelection.collapsed( - offset: MethodSnippet.visible.length + 1, - ), - ), - ); + + expect(controller.historyController.stack[1].code, code1); + expect(controller.historyController.stack[2].code, code2); }); testWidgets('Line count change', (WidgetTester wt) async { final controller = await pumpController(wt, MethodSnippet.full); - await wt.cursorEnd(); - controller.value = controller.value.typed('\n'); - final code1 = controller.code; + await wt.cursorEnd(); // Creates. + + expect(controller.historyController.stack.length, 1); + controller.value = controller.value.typed('\n'); // Creates. + final code1 = controller.code; + controller.value = controller.value.typed('\n'); // Creates. final code2 = controller.code; + await wt.sendKeyEvent(LogicalKeyboardKey.backspace); // Creates. - expect(controller.historyController.stack.length, 3); - expect( - controller.historyController.stack[1], - CodeHistoryRecord( - code: code1, - selection: const TextSelection.collapsed( - offset: MethodSnippet.visible.length + 1, - ), - ), - ); - expect( - controller.historyController.stack[2], - CodeHistoryRecord( - code: code2, - selection: const TextSelection.collapsed( - offset: MethodSnippet.visible.length + 2, - ), - ), - ); + expect(controller.historyController.stack.length, 4); + expect(controller.historyController.stack[1].code, code1); + expect(controller.historyController.stack[2].code, code2); }); testWidgets('Typing + Timeout', (WidgetTester wt) async { @@ -126,6 +111,8 @@ void main() { int? recordCountAfterFirstIdle; await wt.cursorEnd(); + expect(controller.historyController.stack.length, 1); + fakeAsync((async) { controller.value = controller.value.typed('a'); @@ -159,7 +146,7 @@ void main() { group('Undo/Redo.', () { testWidgets('Cannot initially', (WidgetTester wt) async { final controller = await pumpController(wt, MethodSnippet.full); - await wt.cursorEnd(); + expect(controller.historyController.stack.length, 0); await wt.sendUndo(); // No effect. @@ -168,7 +155,6 @@ void main() { controller.selection, const TextSelection.collapsed( offset: MethodSnippet.visible.length, - affinity: TextAffinity.upstream, ), ); @@ -179,7 +165,6 @@ void main() { controller.selection, const TextSelection.collapsed( offset: MethodSnippet.visible.length, - affinity: TextAffinity.upstream, ), ); }); @@ -188,18 +173,25 @@ void main() { final controller = await pumpController(wt, MethodSnippet.full); await wt.cursorEnd(); + expect(controller.historyController.stack.length, 1); + controller.value = controller.value.typed('a'); await wt.moveCursor(-1); // Creates. + expect(controller.historyController.stack.length, 2); + controller.value = controller.value.typed('b'); await wt.moveCursor(-1); // Creates. + expect(controller.historyController.stack.length, 3); + controller.value = controller.value.typed('c'); await wt.sendUndo(); // Creates. expect(controller.historyController.stack.length, 4); expect(controller.fullText, MethodSnippet.full + 'ba'); + // \ selection expect( controller.selection, const TextSelection.collapsed( @@ -210,22 +202,31 @@ void main() { await wt.sendUndo(); expect(controller.fullText, MethodSnippet.full + 'a'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 1, + ), + ); - await wt.sendUndo(); // To initial. + await wt.sendUndo(); expect(controller.fullText, MethodSnippet.full); + // \ selection expect( controller.selection, - const TextSelection.collapsed(offset: -1), + const TextSelection.collapsed( + offset: MethodSnippet.visible.length, + ), ); - await wt.sendUndo(); // No effect. - expect(controller.fullText, MethodSnippet.full); await wt.sendRedo(); expect(controller.fullText, MethodSnippet.full + 'a'); + // \ selection expect( controller.selection, const TextSelection.collapsed( @@ -236,28 +237,55 @@ void main() { await wt.sendRedo(); expect(controller.fullText, MethodSnippet.full + 'ba'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 1, + ), + ); await wt.sendRedo(); expect(controller.fullText, MethodSnippet.full + 'cba'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 1, + ), + ); - await wt.sendRedo(); // No effect. + await wt.sendRedo(); // does nothing expect(controller.fullText, MethodSnippet.full + 'cba'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 1, + ), + ); }); testWidgets('Changing text disables redo', (WidgetTester wt) async { final controller = await pumpController(wt, MethodSnippet.full); await wt.cursorEnd(); + expect(controller.historyController.stack.length, 1); + controller.value = controller.value.typed('a'); await wt.moveCursor(-1); // Creates. + expect(controller.historyController.stack.length, 2); controller.value = controller.value.typed('b'); + expect(controller.historyController.stack.length, 2); await wt.sendUndo(); // Creates. + expect(controller.historyController.stack.length, 3); expect(controller.fullText, MethodSnippet.full + 'a'); + // \ selection expect( controller.selection, const TextSelection.collapsed( @@ -266,16 +294,34 @@ void main() { ); controller.value = controller.value.typed('b'); // Deletes redo records. + expect(controller.fullText, MethodSnippet.full + 'ab'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 2, + ), + ); expect(controller.historyController.stack.length, 2); await wt.sendRedo(); // No effect. + expect(controller.fullText, MethodSnippet.full + 'ab'); + // \ selection + expect( + controller.selection, + const TextSelection.collapsed( + offset: MethodSnippet.visible.length + 2, + ), + ); expect(controller.historyController.stack.length, 2); await wt.moveCursor(-1); // Creates. + expect(controller.historyController.stack.length, 3); expect(controller.fullText, MethodSnippet.full + 'ab'); + // \ selection expect( controller.selection, const TextSelection.collapsed( @@ -298,6 +344,7 @@ void main() { final controller = await pumpController(wt, MethodSnippet.full); await wt.cursorEnd(); + controller.value = controller.value.typed('a'); await wt.sendUndo(); // Creates. await wt.pumpAndSettle();