diff --git a/src/jes/core/databuffer.d b/src/jes/core/databuffer.d index e059cda..a861fa0 100644 --- a/src/jes/core/databuffer.d +++ b/src/jes/core/databuffer.d @@ -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. * diff --git a/src/jes/core/undobuffer.d b/src/jes/core/undobuffer.d new file mode 100644 index 0000000..deaaabf --- /dev/null +++ b/src/jes/core/undobuffer.d @@ -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(); + } +}