Skip to content

Commit

Permalink
add replaceNoHistory, fix push semantics
Browse files Browse the repository at this point in the history
  • Loading branch information
ArthaTi committed Nov 7, 2024
1 parent a3901e3 commit e37e365
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 87 deletions.
3 changes: 2 additions & 1 deletion source/fluid/code_input.d
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,9 @@ class CodeInput : TextInput {

}

protected override void replace(size_t start, size_t end, Rope added, bool) {
protected override void replace(size_t start, size_t end, Rope added, bool isMinor) {

super.replace(start, end, added, isMinor);
reparse(start, end, added);

}
Expand Down
214 changes: 128 additions & 86 deletions source/fluid/text_input.d
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ class TextInput : InputNode!Node, FluidScrollable {
/// Entries will not be merged together unless they are marked as minor. Two such entries can be combined
/// if they are:
///
/// 1. Both additive, and the latter is not subtractive. This combines runs input, including if the first
/// item in the run replaces some text. However, replacing text will break an existing chain of actions.
/// 1. Both additive, and the latter is not subtractive. This combines runs of inserts, including if
/// the first item in the run replaces some text. However, replacing text will break an existing
/// chain of actions.
/// 2. Both subtractive, and neither is additive.
///
/// See_Also: `isAdditive`
Expand Down Expand Up @@ -472,82 +473,49 @@ class TextInput : InputNode!Node, FluidScrollable {
in (start <= end, "Start must be lower than end")
do {

// Single line mode — filter vertical space out
if (!multiline) {

auto lines = newValue.byLine;

if (lines.front.length < newValue.length) {
newValue = lines.join(' ');
}

}

// Nothing changed, ignore this request
if (start == end && newValue.length == 0) return;

const oldValue = contentLabel.value[start..end];
const previousSnapshot = snapshot;

// Perform the replace
contentLabel.replace(start, end, newValue);

// Update caret index
if (caretIndex > start) {

if (caretIndex <= end)
caretIndex = start + newValue.length;
else
caretIndex = caretIndex + start + newValue.length - end;

updateCaretPositionAndAnchor();

}
if (!replaceNoHistory(start, end, newValue, minor)) return;

// Update history
auto snapshot = HistoryEntry(value, selectionStart, selectionEnd, minor);

if (_snapshot.canMergeWith(snapshot)) {

// Try to merge changes with the current revision
// TODO This isn't really a merge
_snapshot = snapshot;

}
else {

// Push a new snapshot
_undoStack.insertBack(_snapshot);
_snapshot = snapshot;
const currentSnapshot = snapshot;

import std.stdio;
debug writefln!"pushing:";
debug writefln!" <-- %s"(_undoStack[]);
debug writefln!" <== %s"(previousSnapshot);
debug writefln!" ==> %s"(currentSnapshot);
debug writefln!" --> %s"(_redoStack[]);

// If the two snapshots are not compatible, insert the previous one into history
if (!previousSnapshot.canMergeWith(currentSnapshot)) {
_undoStack.insertBack(cast() previousSnapshot);
}

// Trigger a resize
_bufferNode = null;
updateSize();

}

/// ditto
void replace(size_t start, size_t end, string newValue) {
void replace(size_t start, size_t end, string newValue, bool minor = false) {

replace(start, end, Rope(newValue));
replace(start, end, Rope(newValue), minor);

}

/// ditto
auto replace() {
auto replace(bool minor = false) {

static struct Replace {

private TextInput input;
private bool isMinor;

Rope opIndexAssign(Rope value, size_t[2] slice) {
input.replace(slice[0], slice[1], value);
input.replace(slice[0], slice[1], value, isMinor);
return value;
}

string opIndexAssign(string value, size_t[2] slice) {
input.replace(slice[0], slice[1], Rope(value));
input.replace(slice[0], slice[1], Rope(value), isMinor);
return value;
}

Expand All @@ -561,7 +529,57 @@ class TextInput : InputNode!Node, FluidScrollable {

}

return Replace(this);
return Replace(this, minor);

}

/// Replace the value without making any changes to edit history.
///
/// The API of this function is not yet stable.
///
/// Returns: True if the value was changed, false otherwise.
protected bool replaceNoHistory(size_t start, size_t end, Rope newValue, bool isMinor = false) {

// Single line mode — filter vertical space out
if (!multiline) {

auto lines = newValue.byLine;

if (lines.front.length < newValue.length) {
newValue = lines.join(' ');
}

}

// Nothing changed, ignore this request
if (start == end && newValue.length == 0) return false;

const oldValue = contentLabel.value[start..end];

// Perform the replace
contentLabel.replace(start, end, newValue);

// Update caret index
if (caretIndex > start) {

if (caretIndex <= end)
caretIndex = start + newValue.length;
else
caretIndex = caretIndex + start + newValue.length - end;

updateCaretPositionAndAnchor();

}

// Update current history entry — this doesn't affect undo/redo history, but is needed to keep integrity
const diff = Rope.DiffRegion(start, oldValue, newValue);
_snapshot = HistoryEntry(value, selectionStart, selectionEnd, isMinor, diff);

// Trigger a resize
_bufferNode = null;
updateSize();

return true;

}

Expand Down Expand Up @@ -605,16 +623,16 @@ class TextInput : InputNode!Node, FluidScrollable {
}

/// Insert text at the given position.
void insert(size_t position, Rope value) {
void insert(size_t position, Rope value, bool minor = false) {

replace(position, position, value);
replace(position, position, value, minor);

}

/// ditto
void insert(size_t position, string value) {
void insert(size_t position, string value, bool minor = false) {

replace(position, position, value);
replace(position, position, value, minor);

}

Expand Down Expand Up @@ -725,8 +743,9 @@ class TextInput : InputNode!Node, FluidScrollable {

const low = selectionLowIndex;
const high = selectionHighIndex;
const isMinor = false;

replace(low, high, newValue);
replace(low, high, newValue, isMinor);
clearSelection();

// Put the caret at the end
Expand Down Expand Up @@ -1358,8 +1377,10 @@ class TextInput : InputNode!Node, FluidScrollable {
// Read text
if (const key = io.inputCharacter) {

const isMinor = true;

// Append to char arrays
push(key);
push(key, isMinor);
changed = true;

}
Expand Down Expand Up @@ -1471,20 +1492,23 @@ class TextInput : InputNode!Node, FluidScrollable {
}

/// Push a character or string to the input.
void push(dchar character) {
void push(dchar character, bool isMinor = true) {

char[4] buffer;

auto size = buffer.encode(character);
push(buffer[0..size]);
push(buffer[0..size], isMinor);

}

/// ditto
void push(scope const(char)[] ch)
void push(scope const(char)[] ch, bool isMinor = true)
out (; _bufferNode, "_bufferNode must exist after pushing to buffer")
do {

// TODO `push` should *not* have isMinor = true as default for API consistency
// it does for backwards compatibility

// Move the buffer node into here; move it back when done
auto bufferNode = _bufferNode;
_bufferNode = null;
Expand Down Expand Up @@ -1513,35 +1537,44 @@ class TextInput : InputNode!Node, FluidScrollable {

}

// The above `if` handles the one case where `push` doesn't directly add new characters to the text.
// The above `if` handles the one case where `push` doesn't just add new characters to the text.
// From here, appending can be optimized by memorizing the node we create to add the text, and reusing it
// afterwards. This way, we avoid creating many one element nodes.
// afterwards. This way, we avoid creating many one character nodes.

size_t originalLength;
const newCaretIndex = caretIndex + ch.length;

// Create a node to write to
if (!bufferNode) {

// Make sure there is a node to write to
if (!bufferNode)
bufferNode = new RopeNode(Rope(slice), Rope.init);

// Insert the node
insert(caretIndex, Rope(bufferNode), isMinor);

}

// If writing in a single sequence, reuse the last inserted node
else {

originalLength = bufferNode.length;
const originalLength = bufferNode.length;

// Append the character to its value
// The bufferNode will always share tail with the buffer
bufferNode.left = usedBuffer[$ - originalLength - ch.length .. $];

}
// Update the node
if (isMinor) {

// Save previous value in undo stack
const previousState = snapshot();
scope (success) pushSnapshot(previousState);
// Since we're replacing the whole node, `replace` won't see it as an additive,
// but as a subsitute change, and create a history entry. Gotta override this.
replaceNoHistory(caretIndex - originalLength, caretIndex, Rope(bufferNode), isMinor);

const newCaretIndex = caretIndex + ch.length;
}
else replace(caretIndex - originalLength, caretIndex, Rope(bufferNode), isMinor);

}

// Insert the text by replacing the old node, if present
replace(caretIndex - originalLength, caretIndex, Rope(bufferNode));
assert(value.isBalanced);

caretIndex = newCaretIndex;
Expand All @@ -1552,10 +1585,6 @@ class TextInput : InputNode!Node, FluidScrollable {
/// ditto
void push(Rope text) {

// Save previous value in undo stack
const previousState = snapshot();
scope (success) pushSnapshot(previousState);

// If selection is active, overwrite the selection
if (isSelecting) {

Expand Down Expand Up @@ -1583,9 +1612,9 @@ class TextInput : InputNode!Node, FluidScrollable {

if (!multiline) return false;

auto snap = snapshot();
push('\n');
forcePushSnapshot(snap);
const isMinor = false;

push('\n', isMinor);

return true;

Expand All @@ -1602,6 +1631,7 @@ class TextInput : InputNode!Node, FluidScrollable {

}

@("breakLine always creates a new history entry")
unittest {

auto root = textInput(.multiline);
Expand All @@ -1611,6 +1641,8 @@ class TextInput : InputNode!Node, FluidScrollable {
assert(root.value == "hello\n");

root.undo();
import std.stdio;
debug writeln(root.value);
assert(root.value == "hello");
root.redo();
assert(root.value == "hello\n");
Expand Down Expand Up @@ -3991,18 +4023,22 @@ class TextInput : InputNode!Node, FluidScrollable {

/// Returns: History entry for the current state.
protected HistoryEntry snapshot() const {
return _snapshot;

HistoryEntry snap = _snapshot;
snap.selectionStart = selectionStart;
snap.selectionEnd = selectionEnd;
return snap;

}

/// Restore state from snapshot.
protected HistoryEntry snapshot(HistoryEntry entry) {

value = entry.value;
// TODO this could be faster
replaceNoHistory(0, value.length, entry.value);
selectSlice(entry.selectionStart, entry.selectionEnd);

return entry;
return _snapshot = entry;

}

Expand All @@ -4013,6 +4049,12 @@ class TextInput : InputNode!Node, FluidScrollable {
// Nothing to undo
if (_undoStack.empty) return;

import std.stdio;
debug writefln!"about to undo:";
debug writefln!" <- %s"(_undoStack[]);
debug writefln!" == %s"(snapshot);
debug writefln!" -> %s"(_redoStack[]);

// Push the current state to redo stack
_redoStack.insertBack(snapshot);

Expand Down

0 comments on commit e37e365

Please sign in to comment.