Skip to content

Commit

Permalink
feat: undo markdown style (#830)
Browse files Browse the repository at this point in the history
* feat: undo style of markdown and retain characters

* test: add tests

* test: macos meta instead of control
  • Loading branch information
Xazin authored Jun 21, 2024
1 parent 52b4959 commit 8279a37
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,21 @@ bool handleFormatByWrappingWithDoubleCharacter({
return false;
}

// Before deletion we need to insert the character in question so that undo manager
// will undo only the style applied and keep the character.
final insertion = editorState.transaction
..insertText(node, lastCharIndex, character)
..afterSelection = Selection.collapsed(
selection.end.copyWith(offset: selection.end.offset + 1),
);
editorState.apply(insertion, skipHistoryDebounce: true);

// if all the conditions are met, we should format the text.
// 1. delete all the *[char]
// 2. update the style of the text surrounded by the double *[char] to [formatStyle]
// 3. update the cursor position.
final deletion = editorState.transaction
..deleteText(node, lastCharIndex, 1)
..deleteText(node, lastCharIndex, 2)
..deleteText(node, thirdLastCharIndex, 2);
editorState.apply(deletion);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,22 @@ bool handleFormatByWrappingWithSingleCharacter({
return false;
}

// Before deletion we need to insert the character in question so that undo manager
// will undo only the style applied and keep the character.
final insertion = editorState.transaction
..insertText(node, selection.end.offset, character)
..afterSelection = Selection.collapsed(
selection.end.copyWith(offset: selection.end.offset + 1),
);
editorState.apply(insertion, skipHistoryDebounce: true);

// If none of the above exclusive conditions are satisfied, we should format the text to [formatStyle].
// 1. Delete the previous 'Character'.
// 2. Update the style of the text surrounded by the two 'Character's to [formatStyle].
// 3. Update the cursor position.

final deletion = editorState.transaction
..deleteText(
node,
lastCharIndex,
1,
);
..deleteText(node, lastCharIndex, 1)
..deleteText(node, selection.end.offset - 1, 1);
editorState.apply(deletion);

// To minimize errors, retrieve the format style from an enum that is specific to single characters.
Expand All @@ -88,10 +93,7 @@ bool handleFormatByWrappingWithSingleCharacter({
}

// if the text is already formatted, we should remove the format.
final sliced = delta.slice(
lastCharIndex + 1,
selection.end.offset,
);
final sliced = delta.slice(lastCharIndex + 1, selection.end.offset);
final result = sliced.everyAttributes((element) => element[style] == true);

final format = editorState.transaction
Expand Down
22 changes: 17 additions & 5 deletions lib/src/editor_state.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'dart:async';
import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/scroll/auto_scroller.dart';
import 'package:appflowy_editor/src/history/undo_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

/// the type of this value is bool.
///
Expand Down Expand Up @@ -284,6 +285,7 @@ class EditorState {
bool isRemote = false,
ApplyOptions options = const ApplyOptions(recordUndo: true),
bool withUpdateSelection = true,
bool skipHistoryDebounce = false,
}) async {
if (!editable || isDisposed) {
return;
Expand Down Expand Up @@ -311,7 +313,7 @@ class EditorState {
_observer.add((TransactionTime.after, transaction));
}

_recordRedoOrUndo(options, transaction);
_recordRedoOrUndo(options, transaction, skipHistoryDebounce);

if (withUpdateSelection) {
_selectionUpdateReason = SelectionUpdateReason.transaction;
Expand Down Expand Up @@ -512,7 +514,11 @@ class EditorState {
}
}

void _recordRedoOrUndo(ApplyOptions options, Transaction transaction) {
void _recordRedoOrUndo(
ApplyOptions options,
Transaction transaction,
bool skipDebounce,
) {
if (options.recordUndo) {
final undoItem = undoManager.getUndoHistoryItem();
undoItem.addAll(transaction.operations);
Expand All @@ -521,7 +527,13 @@ class EditorState {
undoItem.beforeSelection = transaction.beforeSelection;
}
undoItem.afterSelection = transaction.afterSelection;
_debouncedSealHistoryItem();
if (skipDebounce && undoManager.undoStack.isNonEmpty) {
Log.editor.debug('Seal history item');
final last = undoManager.undoStack.last;
last.seal();
} else {
_debouncedSealHistoryItem();
}
} else if (options.recordRedo) {
final redoItem = HistoryItem();
redoItem.addAll(transaction.operations);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:flutter/services.dart';

import 'package:flutter_test/flutter_test.dart';

import 'package:appflowy_editor/appflowy_editor.dart';

import '../../../../infra/testable_editor.dart';

void main() async {
group('undo_markdown_test.dart', () {
testWidgets('single character markdown shortcut then undo', (tester) async {
const helloWorld = "_Hello world_";

final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();

await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.ime.typeText('_Hello world');
await editor.ime.typeText('_');

Delta delta = editor.nodeAtPath([0])!.delta!;
expect(delta.length, 'Hello world'.length);
expect(delta.first.attributes![AppFlowyRichTextKeys.italic], true);

final key = PlatformExtension.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control;
await tester.sendKeyDownEvent(key);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyZ);
await tester.sendKeyUpEvent(key);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyZ);

delta = editor.nodeAtPath([0])!.delta!;
expect(delta.length, helloWorld.length);
expect(delta.toPlainText(), helloWorld);
expect(delta.first.attributes?[AppFlowyRichTextKeys.italic], null);

await editor.dispose();
});

testWidgets('multi character markdown shortcut then undo', (tester) async {
const helloWorld = "__Hello world__";

final editor = tester.editor..addEmptyParagraph();
await editor.startTesting();

await editor.updateSelection(Selection.collapsed(Position(path: [0])));
await editor.ime.typeText('__Hello world_');
await editor.ime.typeText('_');

Delta delta = editor.nodeAtPath([0])!.delta!;
expect(delta.length, 'Hello world'.length);
expect(delta.first.attributes![AppFlowyRichTextKeys.bold], true);

final key = PlatformExtension.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control;
await tester.sendKeyDownEvent(key);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyZ);
await tester.sendKeyUpEvent(key);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyZ);

delta = editor.nodeAtPath([0])!.delta!;
expect(delta.length, helloWorld.length);
expect(delta.toPlainText(), helloWorld);
expect(delta.first.attributes?[AppFlowyRichTextKeys.italic], null);

await editor.dispose();
});
});
}

0 comments on commit 8279a37

Please sign in to comment.