Skip to content

Commit

Permalink
WIP: Still working through undo/redo controller tests
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll committed Jul 12, 2022
1 parent e128f37 commit 39b9194
Show file tree
Hide file tree
Showing 3 changed files with 434 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,47 @@ class ToggleSelectionAttributionsCommand extends AttributedTextEditingValueComma
}
}

/// Removes all attributions within the current selection.
class RemoveSelectedAttributionsCommand extends AttributedTextEditingValueCommand {
RemoveSelectedAttributionsCommand();

Set<AttributionSpan>? _previousAttributionSpans;

@override
AttributedTextEditingValue doExecute(AttributedTextEditingValue previousValue) {
final selectionRange = previousValue.selection.toSpanRange();
_previousAttributionSpans =
previousValue.text.getAttributionSpansInRange(attributionFilter: (_) => true, range: selectionRange).toSet();
final newText = previousValue.text.copy()..clearAttributions(selectionRange);

return AttributedTextEditingValue(
text: newText,
selection: previousValue.selection,
composingRegion: previousValue.composingRegion,
);
}

@override
AttributedTextEditingValue doUndo(AttributedTextEditingValue currentValue) {
final revertedText = currentValue.text.copy();
for (final attributionSpan in _previousAttributionSpans!) {
revertedText.addAttribution(
attributionSpan.attribution,
SpanRange(
start: attributionSpan.start,
end: attributionSpan.end,
),
);
}

return AttributedTextEditingValue(
text: revertedText,
selection: currentValue.selection,
composingRegion: currentValue.composingRegion,
);
}
}

/// Inserts the given text at the current caret location.
///
/// The [AttributedTextEditingValue] must have a collapsed selection.
Expand Down Expand Up @@ -102,7 +143,7 @@ class InsertTextAtCaretCommand extends AttributedTextEditingValueCommand {
selection: TextSelection.collapsed(
offset: previousValue.selection.extentOffset + textToInsert.text.length,
),
composingRegion: composingRegion ?? TextRange.empty,
composingRegion: composingRegion ?? _previousComposingRegion!,
);

return newValue;
Expand Down Expand Up @@ -163,7 +204,7 @@ class InsertTextAtOffsetCommand extends AttributedTextEditingValueCommand {
startOffset: insertionOffset,
),
selection: selectionAfter,
composingRegion: composingRegion ?? TextRange.empty,
composingRegion: composingRegion ?? _previousComposingRegion!,
);

return newValue;
Expand Down Expand Up @@ -196,9 +237,55 @@ extension on AttributedText {
}
}

/// Deletes the currently selected text, collapsing the selection to a caret
/// at the selection base.
class DeleteSelectedTextCommand extends AttributedTextEditingValueCommand {
DeleteSelectedTextCommand({
this.newComposingRegion,
});

final TextRange? newComposingRegion;

AttributedText? _deletedText;
TextSelection? _previousSelection;
TextRange? _previousComposingRegion;

@override
AttributedTextEditingValue doExecute(AttributedTextEditingValue previousValue) {
_deletedText = previousValue.text.copyText(
previousValue.selection.start,
previousValue.selection.end,
);

_previousSelection = previousValue.selection;
_previousComposingRegion = previousValue.composingRegion;

return AttributedTextEditingValue(
text: previousValue.text.removeRegion(
startOffset: previousValue.selection.start,
endOffset: previousValue.selection.end,
),
selection: TextSelection.collapsed(offset: previousValue.selection.baseOffset),
composingRegion: newComposingRegion ?? _previousComposingRegion!,
);
}

@override
AttributedTextEditingValue doUndo(AttributedTextEditingValue currentValue) {
return AttributedTextEditingValue(
text: currentValue.text.insert(
textToInsert: _deletedText!,
startOffset: _previousSelection!.baseOffset,
),
selection: _previousSelection!,
composingRegion: _previousComposingRegion!,
);
}
}

/// Replaces the text, selection, and composing region in the editable.
class ReplaceContentCommands extends AttributedTextEditingValueCommand {
ReplaceContentCommands({
class ReplaceEverythingCommand extends AttributedTextEditingValueCommand {
ReplaceEverythingCommand({
required this.newText,
required this.newSelection,
this.newComposingRegion = TextRange.empty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,26 @@ import '../attributed_text_editing_value.dart';
import 'commands.dart';
import 'event_source_value.dart';

/// An [AttributedTextEditingController] that supports undo/redo.
class EventSourcedAttributedTextEditingController with ChangeNotifier implements AttributedTextEditingController {
EventSourcedAttributedTextEditingController(
AttributedTextEditingValue initialValue,
) : _value = EventSourcedAttributedTextEditingValue(initialValue);

final EventSourcedAttributedTextEditingValue _value;

/// Whether there are any commands in the history stack.
bool get isUndoable => _value.isUndoable;

/// Pops the top command off the history stack and reverses its
/// effect on the current attributed text editing value.
bool undo() => _value.undo();

/// Whether there are any commands in the future stack.
bool get isRedoable => _value.isRedoable;

/// Pops the top command off the future stack and re-applies its
/// effect on the current attributed text editing value.
bool redo() => _value.redo();

@override
Expand All @@ -50,7 +57,7 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
@override
set composingRegion(TextRange _) => throw UnimplementedError();

// TODO: this should probably an extension method on AttributedText or something
// TODO: this should probably be an extension method on AttributedText or something
// like that.
@override
bool isSelectionWithinTextBounds(TextSelection selection) {
Expand Down Expand Up @@ -177,7 +184,7 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
return;
}

// TODO: create a command
_value.execute(RemoveSelectedAttributionsCommand());
}

@override
Expand All @@ -192,7 +199,7 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
TextRange composingRegion = TextRange.empty,
}) {
_value.execute(
ReplaceContentCommands(
ReplaceEverythingCommand(
newText: text,
newSelection: selection,
newComposingRegion: composingRegion,
Expand Down Expand Up @@ -354,7 +361,19 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
return;
}

// TODO: create a command
final upstreamAttributions = _value.text.getAllAttributionsAt(
max(selection.start - 1, 0),
);
final newStyledText = AttributedText(text: replacementText);
final newTextRange = SpanRange(start: 0, end: newStyledText.text.length - 1);
for (final attribution in upstreamAttributions) {
newStyledText.addAttribution(attribution, newTextRange);
}

replaceSelectionWithAttributedText(
attributedReplacementText: newStyledText,
newComposingRegion: newComposingRegion,
);
}

/// Replaces the currently selected text with [attributedReplacementText] and
Expand All @@ -370,7 +389,10 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
return;
}

// TODO: create a command
_value.execute(BatchCommand([
DeleteSelectedTextCommand(),
InsertTextAtCaretCommand(attributedReplacementText, composingRegion: newComposingRegion),
]));
}

/// Replaces the currently selected text with un-styled [text] and collapses
Expand All @@ -386,7 +408,10 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
return;
}

// TODO: create a command
_value.execute(BatchCommand([
DeleteSelectedTextCommand(),
InsertTextAtCaretCommand(AttributedText(text: replacementText), composingRegion: newComposingRegion),
]));
}

/// Removes the text between [from] (inclusive) and [to] (exclusive), and replaces that
Expand All @@ -409,6 +434,8 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
// TODO: create a command
}

/// Deletes all the text on the current line that appears upstream from the
/// caret.
@override
void deleteTextOnLineBeforeCaret({
required ProseTextLayout textLayout,
Expand All @@ -418,13 +445,28 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
// TODO: create a command
}

// TODO: either this method or the next one should be deleted
@override
void deleteSelectedText() {
assert(!selection.isCollapsed);

// TODO: create a command
}

/// Deletes the text within the current [selection].
///
/// Does nothing if [selection] is collapsed.
@override
void deleteSelection({
TextRange? newComposingRegion,
}) {
if (selection.isCollapsed) {
return;
}

// TODO: create a command
}

@override
void deleteCharacter(TextAffinity direction) {
assert(selection.isCollapsed);
Expand Down Expand Up @@ -476,20 +518,6 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
// TODO: create a command
}

/// Deletes the text within the current [selection].
///
/// Does nothing if [selection] is collapsed.
@override
void deleteSelection({
TextRange? newComposingRegion,
}) {
if (selection.isCollapsed) {
return;
}

// TODO: create a command
}

/// Removes the text between [from] (inclusive) and [to] (exclusive).
///
/// The [selection] is updated to [newSelection], if provided, otherwise
Expand All @@ -508,18 +536,29 @@ class EventSourcedAttributedTextEditingController with ChangeNotifier implements
// TODO: create a command
}

/// Sets the text to empty and removes the selection and composing region.
@override
void clear() {
update(
text: AttributedText(text: ""),
selection: const TextSelection.collapsed(offset: -1),
composingRegion: TextRange.empty,
);
}

@override
void update({
AttributedText? text,
TextSelection? selection,
TextRange? composingRegion,
}) {
// TODO: create a command
}

@override
void clear() {
// TODO: create a command
_value.execute(
ReplaceEverythingCommand(
newText: text ?? this.text,
newSelection: selection ?? this.selection,
newComposingRegion: composingRegion ?? this.composingRegion,
),
);
}

@override
Expand Down
Loading

0 comments on commit 39b9194

Please sign in to comment.