1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-16 06:25:01 +00:00

LibGUI: Consolidate and simplify commands used for insertion/removal

This patch adds InsertTextCommand and RemoveTextCommand.
These two commands are used to ... insert and remove text :^)

The bulk of the logic is moved into GTextDocument, and we now use the
command's redo() virtual to perform the action. Or in other words, when
you type into the text editor, we create an InsertTextCommand, push it
onto the undo stack, and call redo() on it immediately. That's how the
text gets inserted.

This makes it quite easy to implement more commands, as there is no
distinction between a redo() and the initial application.
This commit is contained in:
Andreas Kling 2019-11-30 16:50:24 +01:00
parent f430da1d45
commit 00a91bb02c
4 changed files with 179 additions and 288 deletions

View file

@ -392,110 +392,164 @@ GTextDocumentUndoCommand::~GTextDocumentUndoCommand()
{
}
InsertCharacterCommand::InsertCharacterCommand(GTextDocument& document, char ch, GTextPosition text_position)
InsertTextCommand::InsertTextCommand(GTextDocument& document, const String& text, const GTextPosition& position)
: GTextDocumentUndoCommand(document)
, m_character(ch)
, m_text_position(text_position)
, m_text(text)
, m_range({ position, position })
{
}
RemoveCharacterCommand::RemoveCharacterCommand(GTextDocument& document, char ch, GTextPosition text_position)
void InsertTextCommand::redo()
{
auto new_cursor = m_document.insert_at(m_range.start(), m_text);
// NOTE: We don't know where the range ends until after doing redo().
// This is okay since we always do redo() after adding this to the undo stack.
m_range.set_end(new_cursor);
m_document.set_all_cursors(new_cursor);
}
void InsertTextCommand::undo()
{
m_document.remove(m_range);
m_document.set_all_cursors(m_range.start());
}
RemoveTextCommand::RemoveTextCommand(GTextDocument& document, const String& text, const GTextRange& range)
: GTextDocumentUndoCommand(document)
, m_character(ch)
, m_text_position(text_position)
, m_text(text)
, m_range(range)
{
}
RemoveLineCommand::RemoveLineCommand(GTextDocument& document, String line_content, GTextPosition text_position, bool has_merged_content)
: GTextDocumentUndoCommand(document)
, m_line_content(move(line_content))
, m_text_position(text_position)
, m_has_merged_content(has_merged_content)
void RemoveTextCommand::redo()
{
m_document.remove(m_range);
m_document.set_all_cursors(m_range.start());
}
CreateLineCommand::CreateLineCommand(GTextDocument& document, Vector<char> line_content, GTextPosition text_position)
: GTextDocumentUndoCommand(document)
, m_line_content(move(line_content))
, m_text_position(text_position)
void RemoveTextCommand::undo()
{
}
void InsertCharacterCommand::undo()
{
m_document.lines()[m_text_position.line()].remove(m_document, (m_text_position.column() - 1));
m_document.notify_did_change();
}
void InsertCharacterCommand::redo()
{
m_document.lines()[m_text_position.line()].insert(m_document, m_text_position.column() - 1, m_character);
}
void RemoveCharacterCommand::undo()
{
m_document.lines()[m_text_position.line()].insert(m_document, m_text_position.column(), m_character);
}
void RemoveCharacterCommand::redo()
{
m_document.lines()[m_text_position.line()].remove(m_document, (m_text_position.column()));
m_document.notify_did_change();
}
void RemoveLineCommand::undo()
{
// Insert back the line
m_document.insert_line(m_text_position.line(), make<GTextDocumentLine>(m_document, m_line_content));
// Remove the merged line contents
if (m_has_merged_content) {
for (int i = m_line_content.length() - 1; i >= 0; i--)
m_document.lines()[m_text_position.line() - 1].remove(m_document, (m_text_position.column()) + i);
}
}
void RemoveLineCommand::redo()
{
// Remove the created line
m_document.remove_line(m_text_position.line());
// Add back the line contents
if (m_has_merged_content) {
for (int i = 0; i < m_line_content.length(); i++)
m_document.lines()[m_text_position.line() - 1].insert(m_document, (m_text_position.column()) + i, m_line_content[i]);
}
}
void CreateLineCommand::undo()
{
// Insert back the created line portion
for (int i = 0; i < m_line_content.size(); i++)
m_document.lines()[m_text_position.line()].insert(m_document, (m_text_position.column() - 1) + i, m_line_content[i]);
// Move the cursor up a row back before the split.
m_document.set_all_cursors({ m_text_position.line(), m_document.lines()[m_text_position.line()].length() });
// Remove the created line
m_document.remove_line(m_text_position.line() + 1);
}
void CreateLineCommand::redo()
{
// Remove the characters that we're inserted back
for (int i = m_line_content.size() - 1; i >= 0; i--)
m_document.lines()[m_text_position.line()].remove(m_document, (m_text_position.column()) + i);
m_document.notify_did_change();
// Then we want to add BACK the created line
m_document.insert_line(m_text_position.line() + 1, make<GTextDocumentLine>(m_document, ""));
for (int i = 0; i < m_line_content.size(); i++)
m_document.lines()[m_text_position.line() + 1].insert(m_document, i, m_line_content[i]);
auto new_cursor = m_document.insert_at(m_range.start(), m_text);
m_document.set_all_cursors(new_cursor);
}
void GTextDocument::update_undo_timer()
{
m_undo_stack.finalize_current_combo();
}
GTextPosition GTextDocument::insert_at(const GTextPosition& position, const StringView& text)
{
GTextPosition cursor = position;
for (int i = 0; i < text.length(); ++i) {
cursor = insert_at(cursor, text[i]);
}
return cursor;
}
GTextPosition GTextDocument::insert_at(const GTextPosition& position, char ch)
{
// FIXME: We need these from GTextEditor!
bool m_automatic_indentation_enabled = true;
int m_soft_tab_width = 4;
bool at_head = position.column() == 0;
bool at_tail = position.column() == line(position.line()).length();
if (ch == '\n') {
if (at_tail || at_head) {
String new_line_contents;
if (m_automatic_indentation_enabled && at_tail) {
int leading_spaces = 0;
auto& old_line = lines()[position.line()];
for (int i = 0; i < old_line.length(); ++i) {
if (old_line.characters()[i] == ' ')
++leading_spaces;
else
break;
}
if (leading_spaces)
new_line_contents = String::repeated(' ', leading_spaces);
}
int row = position.line();
Vector<char> line_content;
for (int i = position.column(); i < line(row).length(); i++)
line_content.append(line(row).characters()[i]);
insert_line(position.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(*this, new_line_contents));
notify_did_change();
return { position.line() + 1, line(position.line() + 1).length() };
}
auto new_line = make<GTextDocumentLine>(*this);
new_line->append(*this, line(position.line()).characters() + position.column(), line(position.line()).length() - position.column());
Vector<char> line_content;
for (int i = 0; i < new_line->length(); i++)
line_content.append(new_line->characters()[i]);
line(position.line()).truncate(*this, position.column());
insert_line(position.line() + 1, move(new_line));
notify_did_change();
return { position.line() + 1, 0 };
}
if (ch == '\t') {
int next_soft_tab_stop = ((position.column() + m_soft_tab_width) / m_soft_tab_width) * m_soft_tab_width;
int spaces_to_insert = next_soft_tab_stop - position.column();
for (int i = 0; i < spaces_to_insert; ++i) {
line(position.line()).insert(*this, position.column(), ' ');
}
notify_did_change();
return { position.line(), next_soft_tab_stop };
}
line(position.line()).insert(*this, position.column(), ch);
notify_did_change();
return { position.line(), position.column() + 1 };
}
void GTextDocument::remove(const GTextRange& unnormalized_range)
{
if (!unnormalized_range.is_valid())
return;
auto range = unnormalized_range.normalized();
// First delete all the lines in between the first and last one.
for (int i = range.start().line() + 1; i < range.end().line();) {
remove_line(i);
range.end().set_line(range.end().line() - 1);
}
if (range.start().line() == range.end().line()) {
// Delete within same line.
auto& line = lines()[range.start().line()];
bool whole_line_is_selected = range.start().column() == 0 && range.end().column() == line.length();
if (whole_line_is_selected) {
line.clear(*this);
} else {
auto before_selection = String(line.characters(), line.length()).substring(0, range.start().column());
auto after_selection = String(line.characters(), line.length()).substring(range.end().column(), line.length() - range.end().column());
StringBuilder builder(before_selection.length() + after_selection.length());
builder.append(before_selection);
builder.append(after_selection);
line.set_text(*this, builder.to_string());
}
} else {
// Delete across a newline, merging lines.
ASSERT(range.start().line() == range.end().line() - 1);
auto& first_line = lines()[range.start().line()];
auto& second_line = lines()[range.end().line()];
auto before_selection = String(first_line.characters(), first_line.length()).substring(0, range.start().column());
auto after_selection = String(second_line.characters(), second_line.length()).substring(range.end().column(), second_line.length() - range.end().column());
StringBuilder builder(before_selection.length() + after_selection.length());
builder.append(before_selection);
builder.append(after_selection);
first_line.set_text(*this, builder.to_string());
remove_line(range.end().line());
}
if (lines().is_empty()) {
append_line(make<GTextDocumentLine>(*this));
}
notify_did_change();
}