Add UndoBuffer class
This commit is contained in:
parent
b2bd75c2b1
commit
997cf5efe0
@ -48,6 +48,16 @@ class DataBuffer
|
||||
return &m_data[offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address of a byte in the data buffer.
|
||||
*
|
||||
* @param offset Byte offset into the data buffer.
|
||||
*/
|
||||
const(ubyte) * address(size_t offset) const
|
||||
{
|
||||
return &m_data[offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase data from the data buffer.
|
||||
*
|
||||
|
331
src/jes/core/undobuffer.d
Normal file
331
src/jes/core/undobuffer.d
Normal file
@ -0,0 +1,331 @@
|
||||
module jes.core.undobuffer;
|
||||
|
||||
import jes.core.databuffer;
|
||||
|
||||
class UndoBuffer
|
||||
{
|
||||
/**
|
||||
* An UndoUnit represents a single contiguous chunk of inserted or deleted
|
||||
* data.
|
||||
*/
|
||||
class UndoUnit
|
||||
{
|
||||
/** Index where operation took place in target buffer. */
|
||||
size_t target_index;
|
||||
|
||||
/** Index of data stored in undo buffer. */
|
||||
size_t undo_buffer_index;
|
||||
|
||||
/** Length of the unit. */
|
||||
size_t length;
|
||||
|
||||
/** Whether the unit represents an insertion or deletion. */
|
||||
bool insert;
|
||||
|
||||
this(size_t target_index, size_t undo_buffer_index, size_t length, bool insert)
|
||||
{
|
||||
this.target_index = target_index;
|
||||
this.undo_buffer_index = undo_buffer_index;
|
||||
this.length = length;
|
||||
this.insert = insert;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An UndoOperation is a collection of one or more UndoUnits.
|
||||
*
|
||||
* All units in an operation are applied together when undoing/redoing the
|
||||
* full operation.
|
||||
*/
|
||||
class UndoOperation
|
||||
{
|
||||
/** ID of parent operation. */
|
||||
size_t parent;
|
||||
|
||||
/** Units in this operation. */
|
||||
UndoUnit[] units;
|
||||
|
||||
/** ID(s) of child operation(s). */
|
||||
size_t[] children;
|
||||
|
||||
this(size_t parent, UndoUnit unit)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.units ~= unit;
|
||||
}
|
||||
}
|
||||
|
||||
/** List of all undo operations in the buffer. */
|
||||
private UndoOperation[] m_operations;
|
||||
|
||||
/** Buffer storing the inserted/deleted data. */
|
||||
private DataBuffer m_data_buffer;
|
||||
|
||||
/** Operation level, nonzero means wait to commit the operation. */
|
||||
private size_t m_operation_level;
|
||||
|
||||
/** Reference to the operation currently being built. */
|
||||
private UndoOperation m_current_operation;
|
||||
|
||||
/** ID of the operation representing the current buffer state. */
|
||||
private size_t m_current_id;
|
||||
|
||||
this()
|
||||
{
|
||||
m_data_buffer = new DataBuffer();
|
||||
m_operations ~= null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the operation level.
|
||||
*
|
||||
* The operation will not be committed until the level returns to 0.
|
||||
*/
|
||||
void push_operation()
|
||||
{
|
||||
m_operation_level++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the operation should be committed and commit it if so.
|
||||
*/
|
||||
private void check_commit()
|
||||
{
|
||||
if (m_operation_level == 0u)
|
||||
{
|
||||
m_current_operation = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease operation level.
|
||||
*
|
||||
* All undo units will be collected in one operation until the operation
|
||||
* level returns to 0.
|
||||
*/
|
||||
void pop_operation()
|
||||
{
|
||||
m_operation_level--;
|
||||
check_commit();
|
||||
}
|
||||
|
||||
private bool augment_current_unit(const(ubyte) * data, size_t length, size_t target_index, bool insert)
|
||||
{
|
||||
/* Nothing to augment if no operation yet in progress. */
|
||||
if (m_current_operation is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Current undo unit is the last undo unit in the current operation. */
|
||||
UndoUnit cuu = m_current_operation.units[$ - 1];
|
||||
|
||||
size_t cuu_target_end_index = cuu.target_index + cuu.length;
|
||||
|
||||
if (cuu.insert)
|
||||
{
|
||||
if (insert)
|
||||
{
|
||||
/* Case 1: current unit is an insert, new unit is an insert,
|
||||
* and the inserted data can be combined with the current unit
|
||||
* data. */
|
||||
if ((cuu.target_index <= target_index) &&
|
||||
(target_index <= cuu_target_end_index))
|
||||
{
|
||||
m_data_buffer.insert(cuu.undo_buffer_index + target_index - cuu.target_index, data, length);
|
||||
cuu.length += length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Case 2: current unit is an insert, new unit is a delete, and
|
||||
* the data to be deleted is part of the inserted data. */
|
||||
if ((cuu.target_index <= target_index) &&
|
||||
((target_index + length) <= cuu_target_end_index))
|
||||
{
|
||||
m_data_buffer.erase(cuu.undo_buffer_index + target_index - cuu.target_index, length);
|
||||
cuu.length -= length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Case 3: current unit is a delete, new unit is a delete, and the
|
||||
* new delete range is congruent to the current unit delete range. */
|
||||
if (!insert)
|
||||
{
|
||||
if (cuu.target_index == target_index)
|
||||
{
|
||||
m_data_buffer.insert(cuu.undo_buffer_index + cuu.length, data, length);
|
||||
}
|
||||
else if ((target_index + length) == cuu.target_index)
|
||||
{
|
||||
m_data_buffer.insert(cuu.undo_buffer_index, data, length);
|
||||
cuu.target_index = target_index;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
cuu.length += length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void record_new_unit(UndoUnit uu)
|
||||
{
|
||||
if (m_current_operation is null)
|
||||
{
|
||||
UndoOperation uo = new UndoOperation(m_current_id, uu);
|
||||
size_t new_id = m_operations.length;
|
||||
m_operations ~= uo;
|
||||
m_current_operation = uo;
|
||||
if (m_current_id > 0u)
|
||||
{
|
||||
m_operations[m_current_id].children ~= new_id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_current_operation.units ~= uu;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a unit of undoable activity.
|
||||
*
|
||||
* @param data The data being inserted/deleted.
|
||||
* @param length Length of the data.
|
||||
* @param target_index Position in the target buffer where the unit takes place.
|
||||
* @param insert Whether this unit is an insertion or a deletion.
|
||||
*/
|
||||
void record_unit(const(ubyte) * data, size_t length, size_t target_index, bool insert)
|
||||
{
|
||||
if (!augment_current_unit(data, length, target_index, insert))
|
||||
{
|
||||
size_t undo_buffer_index = m_data_buffer.size;
|
||||
m_data_buffer.insert(undo_buffer_index, data, length);
|
||||
UndoUnit uu = new UndoUnit(target_index, undo_buffer_index, length, insert);
|
||||
record_new_unit(uu);
|
||||
}
|
||||
check_commit();
|
||||
}
|
||||
|
||||
version(unittest) invariant
|
||||
{
|
||||
assert(m_operations !is null);
|
||||
assert(m_operations.length > 0u);
|
||||
}
|
||||
|
||||
version(unittest) private @property size_t n_operations() const
|
||||
{
|
||||
return m_operations.length - 1u;
|
||||
}
|
||||
|
||||
version(unittest) private @property size_t n_units() const
|
||||
{
|
||||
size_t n;
|
||||
foreach (operation; m_operations)
|
||||
{
|
||||
if (operation !is null)
|
||||
{
|
||||
n += operation.units.length;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
version(unittest) void record_unit(string data, size_t length, size_t target_index, bool insert)
|
||||
{
|
||||
return record_unit(cast(const(ubyte) *)data.ptr, length, target_index, insert);
|
||||
}
|
||||
|
||||
version(unittest) string uu_string(UndoUnit uu) const
|
||||
{
|
||||
return (cast(immutable(char) *)m_data_buffer.address(uu.undo_buffer_index))[0 .. uu.length];
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
UndoBuffer ub;
|
||||
|
||||
/* Each record_unit creates an operation when level == 0 */
|
||||
ub = new UndoBuffer();
|
||||
assert(ub.n_operations == 0u);
|
||||
assert(ub.n_units == 0u);
|
||||
|
||||
ub.record_unit("hi", 2u, 0u, true);
|
||||
assert(ub.n_operations == 1u);
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.uu_string(ub.m_operations[1].units[0]) == "hi");
|
||||
|
||||
ub.record_unit("there", 5u, 2u, true);
|
||||
assert(ub.n_operations == 2u);
|
||||
assert(ub.n_units == 2u);
|
||||
assert(ub.uu_string(ub.m_operations[2].units[0]) == "there");
|
||||
|
||||
/* Multiple record_unit calls are combined into one operation until
|
||||
* opertaion level is popped. */
|
||||
ub = new UndoBuffer();
|
||||
assert(ub.n_operations == 0u);
|
||||
assert(ub.n_units == 0u);
|
||||
|
||||
ub.push_operation();
|
||||
|
||||
ub.record_unit("hi", 2u, 0u, true);
|
||||
assert(ub.n_operations == 1u);
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.m_operations[1].units[0].target_index == 0u);
|
||||
|
||||
ub.record_unit("there", 5u, 2u, true);
|
||||
assert(ub.n_operations == 1u);
|
||||
|
||||
/* Consecutive inserts are combined into one unit. */
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.uu_string(ub.m_operations[1].units[0]) == "hithere");
|
||||
assert(ub.m_operations[1].units[0].target_index == 0u);
|
||||
|
||||
/* Delete from end of last unit modifies last unit. */
|
||||
ub.record_unit("e", 1u, 6u, false);
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.uu_string(ub.m_operations[1].units[0]) == "hither");
|
||||
assert(ub.m_operations[1].units[0].target_index == 0u);
|
||||
|
||||
/* Delete from beginning of last unit modifies last unit. */
|
||||
ub.record_unit("h", 1u, 0u, false);
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.uu_string(ub.m_operations[1].units[0]) == "ither");
|
||||
assert(ub.m_operations[1].units[0].target_index == 0u);
|
||||
|
||||
/* Delete from middle of last unit modifies last unit. */
|
||||
ub.record_unit("h", 1u, 2u, false);
|
||||
assert(ub.n_units == 1u);
|
||||
assert(ub.uu_string(ub.m_operations[1].units[0]) == "iter");
|
||||
assert(ub.m_operations[1].units[0].target_index == 0u);
|
||||
|
||||
ub.pop_operation();
|
||||
|
||||
/* Congruent deletions are combined. */
|
||||
ub.push_operation();
|
||||
|
||||
ub.record_unit("e", 1u, 2u, false);
|
||||
assert(ub.n_units == 2u);
|
||||
assert(ub.uu_string(ub.m_operations[2].units[0]) == "e");
|
||||
assert(ub.m_operations[2].units[0].target_index == 2u);
|
||||
ub.record_unit("r", 1u, 2u, false);
|
||||
assert(ub.n_units == 2u);
|
||||
assert(ub.uu_string(ub.m_operations[2].units[0]) == "er");
|
||||
assert(ub.m_operations[2].units[0].target_index == 2u);
|
||||
ub.record_unit("t", 1u, 1u, false);
|
||||
assert(ub.n_units == 2u);
|
||||
assert(ub.uu_string(ub.m_operations[2].units[0]) == "ter");
|
||||
assert(ub.m_operations[2].units[0].target_index == 1u);
|
||||
|
||||
ub.pop_operation();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user