From 69317ce8585f7db97e6b9e260f42b051e43f624d Mon Sep 17 00:00:00 2001 From: Alan Mantoux Date: Mon, 4 Dec 2023 19:31:20 +0100 Subject: [PATCH] Add auto formats to history --- .../fleather/lib/src/widgets/autoformats.dart | 31 ++++++---- .../fleather/lib/src/widgets/controller.dart | 16 ++--- .../test/widgets/autoformats_test.dart | 60 ++++++++----------- .../test/widgets/controller_test.dart | 18 ++++++ 4 files changed, 69 insertions(+), 56 deletions(-) diff --git a/packages/fleather/lib/src/widgets/autoformats.dart b/packages/fleather/lib/src/widgets/autoformats.dart index 40965623..de8d59d1 100644 --- a/packages/fleather/lib/src/widgets/autoformats.dart +++ b/packages/fleather/lib/src/widgets/autoformats.dart @@ -16,10 +16,11 @@ abstract class AutoFormat { /// Indicates whether character trigger auto format is kept in document /// /// E.g: for link detections, '[space]' is kept whereas for Markdown block - /// shortcuts, the '[space]' is not added to document + /// shortcuts, the '[space]' is not added to document, it only serves to + /// trigger the block formatting bool get keepTriggerCharacter; - /// Upon upon insertion of a space or new line run format detection and appy + /// Upon upon insertion of a space or new line run format detection and apply /// formatting to document /// Returns a [ActiveFormatResult]. AutoFormatResult? apply( @@ -44,28 +45,36 @@ class AutoFormats { AutoFormatResult? _activeSuggestion; + /// The selection override of the active formatting suggestion + TextSelection? get selection => _activeSuggestion!.selection; + + /// The position at with the active suggestion can be deactivated int get undoPosition => _activeSuggestion!.undoPositionCandidate; + /// `true` if the active suggestion auto format keeps trigger character in + /// document; `false` otherwise bool get activeSuggestionKeepTriggerCharacter => _activeSuggestion!.keepTriggerCharacter; + /// `true` if there is an active auto format suggestion; `false` otherwise bool get hasActiveSuggestion => _activeSuggestion != null; /// Perform detection of auto formats and apply changes to [document] /// /// Inserted data must be of type [String] - TextSelection? run(ParchmentDocument document, int position, Object data) { + /// Returns `true` if auto format was activated + bool run(ParchmentDocument document, int position, Object data) { if (data is! String || data.isEmpty) { - return null; + return false; } for (final autoFormat in _autoFormats) { _activeSuggestion = autoFormat.apply(document, position, data); if (_activeSuggestion != null) { - return _activeSuggestion!.selection; + return true; } } - return null; + return false; } /// Remove auto format from [document] and de-activate current suggestion @@ -168,7 +177,7 @@ class _AutoFormatLinks extends AutoFormat { } } -/// Replaces certain Markdown shortcuts with actual line or block styles. +// Replaces certain Markdown shortcuts with actual line or block styles. class _MarkdownShortCuts extends AutoFormat { static final rules = { '-': ParchmentAttribute.block.bulletList, @@ -302,8 +311,8 @@ class _MarkdownShortCuts extends AutoFormat { } } -/// Skips to the beginning of line containing position at specified [length] -/// and returns contents of the line skipped so far. +// Skips to the beginning of line containing position at specified [length] +// and returns contents of the line skipped so far. List skipToLineAt(DeltaIterator iter, int length) { if (length == 0) { return List.empty(growable: false); @@ -333,8 +342,8 @@ List skipToLineAt(DeltaIterator iter, int length) { return prefix; } -/// Infers text direction from the input when happens in the beginning of a line. -/// This rule also removes alignment and sets it based on inferred direction. +// Infers text direction from the input when happens in the beginning of a line. +// This rule also removes alignment and sets it based on inferred direction. class _AutoTextDirection extends AutoFormat { const _AutoTextDirection(); diff --git a/packages/fleather/lib/src/widgets/controller.dart b/packages/fleather/lib/src/widgets/controller.dart index e029b067..7b5ca677 100644 --- a/packages/fleather/lib/src/widgets/controller.dart +++ b/packages/fleather/lib/src/widgets/controller.dart @@ -40,7 +40,7 @@ class FleatherController extends ChangeNotifier { late final _Throttled _throttledPush; Timer? _throttleTimer; - // The autoformat handler + // The auto format handler final AutoFormats _autoFormats; /// Currently selected text within the [document]. @@ -120,6 +120,7 @@ class FleatherController extends ChangeNotifier { final isDataNotEmpty = data is String ? data.isNotEmpty : true; if (!_captureAutoFormatCancellationOrUndo(document, index, length, data)) { + _updateHistory(); notifyListeners(); return; } @@ -152,12 +153,12 @@ class FleatherController extends ChangeNotifier { ), source: ChangeSource.local, ); + final autoFormatPerformed = _autoFormats.run(document, index, data); // Only update history when text is being updated // We do not want to update it when selection is changed _updateHistory(); - final autoFormatSelection = _autoFormats.run(document, index, data); - if (autoFormatSelection != null) { - _updateSelectionSilent(autoFormatSelection, + if (autoFormatPerformed && _autoFormats.selection != null) { + _updateSelectionSilent(_autoFormats.selection!, source: ChangeSource.local); } } @@ -329,12 +330,7 @@ extension HistoryHandler on FleatherController { source: ChangeSource.history); } - void _updateHistory({bool forceNewEntry = false}) { - if (forceNewEntry) { - _history.push(document.toDelta()); - return; - } - + void _updateHistory() { if (plainTextEditingValue == TextEditingValue.empty) { return; } diff --git a/packages/fleather/test/widgets/autoformats_test.dart b/packages/fleather/test/widgets/autoformats_test.dart index 04459348..1e440e30 100644 --- a/packages/fleather/test/widgets/autoformats_test.dart +++ b/packages/fleather/test/widgets/autoformats_test.dart @@ -14,8 +14,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text and a https://fleather-editor.github.io\n'} ]); - final selection = autoformats.run(document, 54, ' '); - expect(selection, isNull); + final performed = autoformats.run(document, 54, ' '); + expect(performed, true); + expect(autoformats.selection, isNull); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.link.key), isTrue); @@ -27,8 +28,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text and a www.github.com\n'} ]); - final selection = autoformats.run(document, 35, ' '); - expect(selection, isNull); + final performed = autoformats.run(document, 35, ' '); + expect(performed, isTrue); + expect(autoformats.selection, isNull); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.link.key), isTrue); @@ -41,8 +43,8 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text and a https://fleather-editor.github.io\n'} ]); - final selection = autoformats.run(document, 54, 'p'); - expect(selection, null); + final performed = autoformats.run(document, 54, 'p'); + expect(performed, false); expect(autoformats.hasActiveSuggestion, isFalse); }); @@ -74,8 +76,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text\n* \nthat continues\n'} ]); - final selection = autoformats.run(document, 16, ' '); - expect(selection, const TextSelection.collapsed(offset: 15)); + final performed = autoformats.run(document, 16, ' '); + expect(performed, true); + expect(autoformats.selection, const TextSelection.collapsed(offset: 15)); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.block.key), isTrue); @@ -91,25 +94,9 @@ void main() { }, {'insert': '\nthat continues\n'} ]); - final selection = autoformats.run(document, 16, ' '); - expect(selection, const TextSelection.collapsed(offset: 16)); - final attributes = document.toDelta().toList()[2].attributes; - expect(attributes, isNotNull); - expect(attributes!.containsKey(ParchmentAttribute.block.key), isTrue); - expect(attributes[ParchmentAttribute.block.key], - ParchmentAttribute.block.bulletList.value); - }); - - test('Ignore if in ', () { - final document = ParchmentDocument.fromJson([ - {'insert': 'Some long text\n* '}, - { - 'insert': SpanEmbed('some', data: {'ok': 'ok'}).toJson() - }, - {'insert': '\nthat continues\n'} - ]); - final selection = autoformats.run(document, 16, ' '); - expect(selection, const TextSelection.collapsed(offset: 16)); + final performed = autoformats.run(document, 16, ' '); + expect(performed, true); + expect(autoformats.selection, const TextSelection.collapsed(offset: 16)); final attributes = document.toDelta().toList()[2].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.block.key), isTrue); @@ -121,8 +108,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text\n1. \nthat continues\n'} ]); - final selection = autoformats.run(document, 17, ' '); - expect(selection, const TextSelection.collapsed(offset: 15)); + final performed = autoformats.run(document, 17, ' '); + expect(performed, true); + expect(autoformats.selection, const TextSelection.collapsed(offset: 15)); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.block.key), isTrue); @@ -134,8 +122,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text\n```\nthat continues\n'} ]); - final selection = autoformats.run(document, 17, '`'); - expect(selection, const TextSelection.collapsed(offset: 15)); + final performed = autoformats.run(document, 17, '`'); + expect(performed, true); + expect(autoformats.selection, const TextSelection.collapsed(offset: 15)); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.block.key), isTrue); @@ -147,8 +136,8 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'Some long text\n* \nthat continues\n'} ]); - final selection = autoformats.run(document, 16, 'p'); - expect(selection, null); + final performed = autoformats.run(document, 16, 'p'); + expect(performed, false); expect(autoformats.hasActiveSuggestion, isFalse); }); @@ -180,8 +169,9 @@ void main() { final document = ParchmentDocument.fromJson([ {'insert': 'some ltr text\nש\n'} ]); - final selection = autoformats.run(document, 14, 'ש'); - expect(selection, isNull); + final performed = autoformats.run(document, 14, 'ש'); + expect(performed, true); + expect(autoformats.selection, isNull); final attributes = document.toDelta().toList()[1].attributes; expect(attributes, isNotNull); expect(attributes!.containsKey(ParchmentAttribute.direction.key), isTrue); diff --git a/packages/fleather/test/widgets/controller_test.dart b/packages/fleather/test/widgets/controller_test.dart index e47c73ff..834807da 100644 --- a/packages/fleather/test/widgets/controller_test.dart +++ b/packages/fleather/test/widgets/controller_test.dart @@ -305,6 +305,24 @@ void main() { expect(controller.selection, selection); }); + test('History undo of link detection', () { + fakeAsync((async) { + const text = 'Some link https://fleather-editor.github.io'; + const selection = TextSelection.collapsed(offset: text.length); + controller.replaceText(0, 0, text, + selection: + const TextSelection.collapsed(offset: text.length - 1)); + async.flushTimers(); + controller.replaceText(text.length, 0, ' ', selection: selection); + async.flushTimers(); + controller.undo(); + expect(controller.document.toDelta().length, 1); + expect(controller.document.toDelta()[0].data, + 'Some link https://fleather-editor.github.io\n'); + expect(controller.document.toDelta()[0].attributes, isNull); + }); + }); + test('Undo link detection', () { const text = 'Some link https://fleather-editor.github.io'; const selection = TextSelection.collapsed(offset: text.length);