From f1c6193d6d5b35942be4732f65db65ac944b064a Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 27 Oct 2019 16:10:07 +0100 Subject: [PATCH] LibGUI: Move GTextDocument out of GTextEditor The idea here is to decouple the document from the editor widget so you could have multiple editors being views onto the same document. This doesn't work yet, since the document and editor are coupled in various ways still (including a per-line back-pointer to the editor.) --- DevTools/HackStudio/main.cpp | 6 +- Libraries/LibGUI/GTextDocument.cpp | 113 ++++++++++++ Libraries/LibGUI/GTextDocument.h | 83 +++++++++ Libraries/LibGUI/GTextEditor.cpp | 274 +++++++++-------------------- Libraries/LibGUI/GTextEditor.h | 65 ++----- Libraries/LibGUI/Makefile | 1 + 6 files changed, 302 insertions(+), 240 deletions(-) create mode 100644 Libraries/LibGUI/GTextDocument.cpp create mode 100644 Libraries/LibGUI/GTextDocument.h diff --git a/DevTools/HackStudio/main.cpp b/DevTools/HackStudio/main.cpp index dd306fa228..9ae7ef47c4 100644 --- a/DevTools/HackStudio/main.cpp +++ b/DevTools/HackStudio/main.cpp @@ -286,12 +286,12 @@ static void rehighlight() CppLexer lexer(text); auto tokens = lexer.lex(); - Vector spans; + Vector spans; for (auto& token : tokens) { #ifdef DEBUG_SYNTAX_HIGHLIGHTING dbg() << token.to_string() << " @ " << token.m_start.line << ":" << token.m_start.column << " - " << token.m_end.line << ":" << token.m_end.column; #endif - GTextEditor::Span span; + GTextDocumentSpan span; span.range.set_start({ token.m_start.line, token.m_start.column }); span.range.set_end({ token.m_end.line, token.m_end.column }); auto style = style_for_token_type(token.m_type); @@ -299,7 +299,7 @@ static void rehighlight() span.font = style.font; spans.append(span); } - current_editor().set_spans(spans); + current_editor().document().set_spans(spans); current_editor().update(); } diff --git a/Libraries/LibGUI/GTextDocument.cpp b/Libraries/LibGUI/GTextDocument.cpp new file mode 100644 index 0000000000..449a7a2142 --- /dev/null +++ b/Libraries/LibGUI/GTextDocument.cpp @@ -0,0 +1,113 @@ +#include +#include + +GTextDocument::GTextDocument(GTextEditor& editor) + : m_editor(editor) +{ + m_lines.append(make(m_editor)); +} + +void GTextDocument::set_text(const StringView& text) +{ + m_spans.clear(); + m_lines.clear(); + int start_of_current_line = 0; + + auto add_line = [&](int current_position) { + int line_length = current_position - start_of_current_line; + auto line = make(m_editor); + if (line_length) + line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line)); + m_lines.append(move(line)); + start_of_current_line = current_position + 1; + }; + int i = 0; + for (i = 0; i < text.length(); ++i) { + if (text[i] == '\n') + add_line(i); + } + add_line(i); +} + +int GTextDocumentLine::first_non_whitespace_column() const +{ + for (int i = 0; i < length(); ++i) { + if (!isspace(m_text[i])) + return i; + } + return length(); +} + +GTextDocumentLine::GTextDocumentLine(GTextEditor& editor) + : m_editor(editor) +{ + clear(); +} + +GTextDocumentLine::GTextDocumentLine(GTextEditor& editor, const StringView& text) + : m_editor(editor) +{ + set_text(text); +} + +void GTextDocumentLine::clear() +{ + m_text.clear(); + m_text.append(0); +} + +void GTextDocumentLine::set_text(const StringView& text) +{ + if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length())) + return; + if (text.is_empty()) { + clear(); + return; + } + m_text.resize(text.length() + 1); + memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1); +} + +void GTextDocumentLine::append(const char* characters, int length) +{ + int old_length = m_text.size() - 1; + m_text.resize(m_text.size() + length); + memcpy(m_text.data() + old_length, characters, length); + m_text.last() = 0; +} + +void GTextDocumentLine::append(char ch) +{ + insert(length(), ch); +} + +void GTextDocumentLine::prepend(char ch) +{ + insert(0, ch); +} + +void GTextDocumentLine::insert(int index, char ch) +{ + if (index == length()) { + m_text.last() = ch; + m_text.append(0); + } else { + m_text.insert(index, move(ch)); + } +} + +void GTextDocumentLine::remove(int index) +{ + if (index == length()) { + m_text.take_last(); + m_text.last() = 0; + } else { + m_text.remove(index); + } +} + +void GTextDocumentLine::truncate(int length) +{ + m_text.resize(length + 1); + m_text.last() = 0; +} diff --git a/Libraries/LibGUI/GTextDocument.h b/Libraries/LibGUI/GTextDocument.h new file mode 100644 index 0000000000..b1bd94246d --- /dev/null +++ b/Libraries/LibGUI/GTextDocument.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class GTextEditor; +class GTextDocumentLine; + +struct GTextDocumentSpan { + GTextRange range; + Color color; + const Font* font { nullptr }; +}; + +class GTextDocument : public RefCounted { +public: + static NonnullRefPtr create(GTextEditor& editor) + { + return adopt(*new GTextDocument(editor)); + } + + int line_count() const { return m_lines.size(); } + const GTextDocumentLine& line(int line_index) const { return m_lines[line_index]; } + GTextDocumentLine& line(int line_index) { return m_lines[line_index]; } + + void set_spans(const Vector& spans) { m_spans = spans; } + + void set_text(const StringView&); + + const NonnullOwnPtrVector& lines() const { return m_lines; } + NonnullOwnPtrVector& lines() { return m_lines; } + + bool has_spans() const { return !m_spans.is_empty(); } + const Vector& spans() const { return m_spans; } + +private: + explicit GTextDocument(GTextEditor&); + + NonnullOwnPtrVector m_lines; + Vector m_spans; + + GTextEditor& m_editor; +}; + +class GTextDocumentLine { + friend class GTextEditor; + friend class GTextDocument; + +public: + explicit GTextDocumentLine(GTextEditor&); + GTextDocumentLine(GTextEditor&, const StringView&); + + StringView view() const { return { characters(), length() }; } + const char* characters() const { return m_text.data(); } + int length() const { return m_text.size() - 1; } + void set_text(const StringView&); + void append(char); + void prepend(char); + void insert(int index, char); + void remove(int index); + void append(const char*, int); + void truncate(int length); + void clear(); + void recompute_visual_lines(); + int visual_line_containing(int column) const; + int first_non_whitespace_column() const; + + template + void for_each_visual_line(Callback) const; + +private: + GTextEditor& m_editor; + + // NOTE: This vector is null terminated. + Vector m_text; + + Vector m_visual_line_breaks; + Rect m_visual_rect; +}; diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp index 40efa48388..abb5b911c2 100644 --- a/Libraries/LibGUI/GTextEditor.cpp +++ b/Libraries/LibGUI/GTextEditor.cpp @@ -19,6 +19,7 @@ GTextEditor::GTextEditor(Type type, GWidget* parent) : GScrollableWidget(parent) , m_type(type) { + m_document = GTextDocument::create(*this); set_frame_shape(FrameShape::Container); set_frame_shadow(FrameShadow::Sunken); set_frame_thickness(2); @@ -26,7 +27,6 @@ GTextEditor::GTextEditor(Type type, GWidget* parent) set_font(GFontDatabase::the().get_by_name("Csilla Thin")); // FIXME: Recompute vertical scrollbar step size on font change. vertical_scrollbar().set_step(line_height()); - m_lines.append(make(*this)); m_cursor = { 0, 0 }; create_actions(); } @@ -51,33 +51,17 @@ void GTextEditor::create_actions() void GTextEditor::set_text(const StringView& text) { - if (is_single_line() && text.length() == m_lines[0].length() && !memcmp(text.characters_without_null_termination(), m_lines[0].characters(), text.length())) + if (is_single_line() && text.length() == line(0).length() && !memcmp(text.characters_without_null_termination(), line(0).characters(), text.length())) return; - m_spans.clear(); - m_selection.clear(); - m_lines.clear(); - int start_of_current_line = 0; - auto add_line = [&](int current_position) { - int line_length = current_position - start_of_current_line; - auto line = make(*this); - if (line_length) - line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line)); - m_lines.append(move(line)); - start_of_current_line = current_position + 1; - }; - int i = 0; - for (i = 0; i < text.length(); ++i) { - if (text[i] == '\n') - add_line(i); - } - add_line(i); + document().set_text(text); + update_content_size(); recompute_all_visual_lines(); if (is_single_line()) - set_cursor(0, m_lines[0].length()); + set_cursor(0, line(0).length()); else set_cursor(0, 0); did_update_selection(); @@ -88,7 +72,7 @@ void GTextEditor::update_content_size() { int content_width = 0; int content_height = 0; - for (auto& line : m_lines) { + for (auto& line : document().lines()) { content_width = max(line.m_visual_rect.width(), content_width); content_height += line.m_visual_rect.height(); } @@ -110,13 +94,13 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const int line_index = -1; if (is_line_wrapping_enabled()) { - for (int i = 0; i < m_lines.size(); ++i) { - auto& rect = m_lines[i].m_visual_rect; + for (int i = 0; i < lines().size(); ++i) { + auto& rect = lines()[i].m_visual_rect; if (position.y() >= rect.top() && position.y() <= rect.bottom()) { line_index = i; break; } else if (position.y() > rect.bottom()) - line_index = m_lines.size() - 1; + line_index = lines().size() - 1; } } else { line_index = position.y() / line_height(); @@ -124,7 +108,7 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const line_index = max(0, min(line_index, line_count() - 1)); - auto& line = m_lines[line_index]; + auto& line = lines()[line_index]; int column_index; switch (m_text_alignment) { @@ -149,7 +133,7 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const ASSERT_NOT_REACHED(); } - column_index = max(0, min(column_index, m_lines[line_index].length())); + column_index = max(0, min(column_index, lines()[line_index].length())); return { line_index, column_index }; } @@ -163,9 +147,9 @@ void GTextEditor::doubleclick_event(GMouseEvent& event) auto start = text_position_at(event.position()); auto end = start; - auto& line = m_lines[start.line()]; + auto& line = lines()[start.line()]; - if (m_spans.is_empty()) { + if (!document().has_spans()) { while (start.column() > 0) { if (isspace(line.characters()[start.column() - 1])) break; @@ -178,7 +162,7 @@ void GTextEditor::doubleclick_event(GMouseEvent& event) end.set_column(end.column() + 1); } } else { - for (auto& span : m_spans) { + for (auto& span : document().spans()) { if (!span.range.contains(start)) continue; start = span.range.start(); @@ -209,11 +193,11 @@ void GTextEditor::mousedown_event(GMouseEvent& event) if (is_multi_line()) { // select *current* line start = GTextPosition(m_cursor.line(), 0); - end = GTextPosition(m_cursor.line(), m_lines[m_cursor.line()].length()); + end = GTextPosition(m_cursor.line(), lines()[m_cursor.line()].length()); } else { // select *whole* line start = GTextPosition(0, 0); - end = GTextPosition(line_count() - 1, m_lines[line_count() - 1].length()); + end = GTextPosition(line_count() - 1, lines()[line_count() - 1].length()); } m_selection.set(start, end); @@ -351,7 +335,7 @@ void GTextEditor::paint_event(GPaintEvent& event) painter.add_clip_rect(text_clip_rect); for (int line_index = first_visible_line; line_index <= last_visible_line; ++line_index) { - auto& line = m_lines[line_index]; + auto& line = lines()[line_index]; bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line(); int first_visual_line_with_selection = -1; @@ -378,7 +362,7 @@ void GTextEditor::paint_event(GPaintEvent& event) #ifdef DEBUG_GTEXTEDITOR painter.draw_rect(visual_line_rect, Color::Cyan); #endif - if (m_spans.is_empty()) { + if (!document().has_spans()) { // Fast-path for plain text painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, Color::Black); } else { @@ -389,7 +373,7 @@ void GTextEditor::paint_event(GPaintEvent& event) Color color; GTextPosition physical_position(line_index, start_of_visual_line + i); // FIXME: This is *horribly* inefficient. - for (auto& span : m_spans) { + for (auto& span : document().spans()) { if (!span.range.contains(physical_position)) continue; color = span.color; @@ -466,7 +450,7 @@ void GTextEditor::toggle_selection_if_needed_for_event(const GKeyEvent& event) void GTextEditor::select_all() { GTextPosition start_of_document { 0, 0 }; - GTextPosition end_of_document { line_count() - 1, m_lines[line_count() - 1].length() }; + GTextPosition end_of_document { line_count() - 1, lines()[line_count() - 1].length() }; m_selection.set(start_of_document, end_of_document); did_update_selection(); set_cursor(end_of_document); @@ -492,7 +476,7 @@ void GTextEditor::keydown_event(GKeyEvent& event) if (event.key() == KeyCode::Key_Up) { if (m_cursor.line() > 0) { int new_line = m_cursor.line() - 1; - int new_column = min(m_cursor.column(), m_lines[new_line].length()); + int new_column = min(m_cursor.column(), lines()[new_line].length()); toggle_selection_if_needed_for_event(event); set_cursor(new_line, new_column); if (event.shift() && m_selection.start().is_valid()) { @@ -503,9 +487,9 @@ void GTextEditor::keydown_event(GKeyEvent& event) return; } if (event.key() == KeyCode::Key_Down) { - if (m_cursor.line() < (m_lines.size() - 1)) { + if (m_cursor.line() < (lines().size() - 1)) { int new_line = m_cursor.line() + 1; - int new_column = min(m_cursor.column(), m_lines[new_line].length()); + int new_column = min(m_cursor.column(), lines()[new_line].length()); toggle_selection_if_needed_for_event(event); set_cursor(new_line, new_column); if (event.shift() && m_selection.start().is_valid()) { @@ -518,7 +502,7 @@ void GTextEditor::keydown_event(GKeyEvent& event) if (event.key() == KeyCode::Key_PageUp) { if (m_cursor.line() > 0) { int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height()); - int new_column = min(m_cursor.column(), m_lines[new_line].length()); + int new_column = min(m_cursor.column(), lines()[new_line].length()); toggle_selection_if_needed_for_event(event); set_cursor(new_line, new_column); if (event.shift() && m_selection.start().is_valid()) { @@ -529,9 +513,9 @@ void GTextEditor::keydown_event(GKeyEvent& event) return; } if (event.key() == KeyCode::Key_PageDown) { - if (m_cursor.line() < (m_lines.size() - 1)) { + if (m_cursor.line() < (lines().size() - 1)) { int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height()); - int new_column = min(m_cursor.column(), m_lines[new_line].length()); + int new_column = min(m_cursor.column(), lines()[new_line].length()); toggle_selection_if_needed_for_event(event); set_cursor(new_line, new_column); if (event.shift() && m_selection.start().is_valid()) { @@ -552,7 +536,7 @@ void GTextEditor::keydown_event(GKeyEvent& event) } } else if (m_cursor.line() > 0) { int new_line = m_cursor.line() - 1; - int new_column = m_lines[new_line].length(); + int new_column = lines()[new_line].length(); toggle_selection_if_needed_for_event(event); set_cursor(new_line, new_column); if (event.shift() && m_selection.start().is_valid()) { @@ -613,7 +597,7 @@ void GTextEditor::keydown_event(GKeyEvent& event) } if (event.ctrl() && event.key() == KeyCode::Key_End) { toggle_selection_if_needed_for_event(event); - set_cursor(line_count() - 1, m_lines[line_count() - 1].length()); + set_cursor(line_count() - 1, lines()[line_count() - 1].length()); if (event.shift() && m_selection.start().is_valid()) { m_selection.set_end(m_cursor); did_update_selection(); @@ -655,10 +639,10 @@ void GTextEditor::keydown_event(GKeyEvent& event) } if (m_cursor.column() == 0 && m_cursor.line() != 0) { // Backspace at column 0; merge with previous line - auto& previous_line = m_lines[m_cursor.line() - 1]; + auto& previous_line = lines()[m_cursor.line() - 1]; int previous_length = previous_line.length(); previous_line.append(current_line().characters(), current_line().length()); - m_lines.remove(m_cursor.line()); + lines().remove(m_cursor.line()); update_content_size(); update(); set_cursor(m_cursor.line() - 1, previous_length); @@ -691,9 +675,9 @@ void GTextEditor::delete_current_line() if (has_selection()) return delete_selection(); - m_lines.remove(m_cursor.line()); - if (m_lines.is_empty()) - m_lines.append(make(*this)); + lines().remove(m_cursor.line()); + if (lines().is_empty()) + lines().append(make(*this)); update_content_size(); update(); @@ -716,10 +700,10 @@ void GTextEditor::do_delete() } if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) { // Delete at end of line; merge with next line - auto& next_line = m_lines[m_cursor.line() + 1]; + auto& next_line = lines()[m_cursor.line() + 1]; int previous_length = current_line().length(); current_line().append(next_line.characters(), next_line.length()); - m_lines.remove(m_cursor.line() + 1); + lines().remove(m_cursor.line() + 1); update(); did_change(); set_cursor(m_cursor.line(), previous_length); @@ -744,7 +728,7 @@ void GTextEditor::insert_at_cursor(char ch) String new_line_contents; if (m_automatic_indentation_enabled && at_tail) { int leading_spaces = 0; - auto& old_line = m_lines[m_cursor.line()]; + auto& old_line = lines()[m_cursor.line()]; for (int i = 0; i < old_line.length(); ++i) { if (old_line.characters()[i] == ' ') ++leading_spaces; @@ -754,16 +738,16 @@ void GTextEditor::insert_at_cursor(char ch) if (leading_spaces) new_line_contents = String::repeated(' ', leading_spaces); } - m_lines.insert(m_cursor.line() + (at_tail ? 1 : 0), make(*this, new_line_contents)); + lines().insert(m_cursor.line() + (at_tail ? 1 : 0), make(*this, new_line_contents)); update(); did_change(); - set_cursor(m_cursor.line() + 1, m_lines[m_cursor.line() + 1].length()); + set_cursor(m_cursor.line() + 1, lines()[m_cursor.line() + 1].length()); return; } - auto new_line = make(*this); + auto new_line = make(*this); new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column()); current_line().truncate(m_cursor.column()); - m_lines.insert(m_cursor.line() + 1, move(new_line)); + lines().insert(m_cursor.line() + 1, move(new_line)); update(); did_change(); set_cursor(m_cursor.line() + 1, 0); @@ -786,7 +770,7 @@ void GTextEditor::insert_at_cursor(char ch) int GTextEditor::content_x_for_position(const GTextPosition& position) const { - auto& line = m_lines[position.line()]; + auto& line = lines()[position.line()]; int x_offset = -1; switch (m_text_alignment) { case TextAlignment::CenterLeft: @@ -811,7 +795,7 @@ Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const { if (!position.is_valid()) return {}; - ASSERT(!m_lines.is_empty()); + ASSERT(!lines().is_empty()); ASSERT(position.column() <= (current_line().length() + 1)); int x = content_x_for_position(position); @@ -822,7 +806,7 @@ Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const return rect; } - auto& line = m_lines[position.line()]; + auto& line = lines()[position.line()]; Rect rect; line.for_each_visual_line([&](const Rect& visual_line_rect, const StringView& view, int start_of_visual_line) { if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) { @@ -862,8 +846,8 @@ void GTextEditor::scroll_position_into_view(const GTextPosition& position) auto rect = content_rect_for_position(position); if (position.column() == 0) rect.set_x(content_x_for_position({ position.line(), 0 }) - 2); - else if (position.column() == m_lines[position.line()].length()) - rect.set_x(content_x_for_position({ position.line(), m_lines[position.line()].length() }) + 2); + else if (position.column() == lines()[position.line()].length()) + rect.set_x(content_x_for_position({ position.line(), lines()[position.line()].length() }) + 2); scroll_into_view(rect, true, true); } @@ -874,7 +858,7 @@ void GTextEditor::scroll_cursor_into_view() Rect GTextEditor::line_content_rect(int line_index) const { - auto& line = m_lines[line_index]; + auto& line = lines()[line_index]; if (is_single_line()) { Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, line.length() * glyph_width(), font().glyph_height() + 2 }; line_rect.center_vertically_within({ {}, frame_inner_rect().size() }); @@ -902,19 +886,19 @@ void GTextEditor::set_cursor(int line, int column) void GTextEditor::set_cursor(const GTextPosition& a_position) { - ASSERT(!m_lines.is_empty()); + ASSERT(!lines().is_empty()); GTextPosition position = a_position; - if (position.line() >= m_lines.size()) - position.set_line(m_lines.size() - 1); + if (position.line() >= lines().size()) + position.set_line(lines().size() - 1); - if (position.column() > m_lines[position.line()].length()) - position.set_column(m_lines[position.line()].length()); + if (position.column() > lines()[position.line()].length()) + position.set_column(lines()[position.line()].length()); if (m_cursor != position) { // NOTE: If the old cursor is no longer valid, repaint everything just in case. - auto old_cursor_line_rect = m_cursor.line() < m_lines.size() + auto old_cursor_line_rect = m_cursor.line() < lines().size() ? line_widget_rect(m_cursor.line()) : rect(); m_cursor = position; @@ -945,79 +929,6 @@ void GTextEditor::timer_event(CTimerEvent&) update_cursor(); } -GTextEditor::Line::Line(GTextEditor& editor) - : m_editor(editor) -{ - clear(); -} - -GTextEditor::Line::Line(GTextEditor& editor, const StringView& text) - : m_editor(editor) -{ - set_text(text); -} - -void GTextEditor::Line::clear() -{ - m_text.clear(); - m_text.append(0); -} - -void GTextEditor::Line::set_text(const StringView& text) -{ - if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length())) - return; - if (text.is_empty()) { - clear(); - return; - } - m_text.resize(text.length() + 1); - memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1); -} - -void GTextEditor::Line::append(const char* characters, int length) -{ - int old_length = m_text.size() - 1; - m_text.resize(m_text.size() + length); - memcpy(m_text.data() + old_length, characters, length); - m_text.last() = 0; -} - -void GTextEditor::Line::append(char ch) -{ - insert(length(), ch); -} - -void GTextEditor::Line::prepend(char ch) -{ - insert(0, ch); -} - -void GTextEditor::Line::insert(int index, char ch) -{ - if (index == length()) { - m_text.last() = ch; - m_text.append(0); - } else { - m_text.insert(index, move(ch)); - } -} - -void GTextEditor::Line::remove(int index) -{ - if (index == length()) { - m_text.take_last(); - m_text.last() = 0; - } else { - m_text.remove(index); - } -} - -void GTextEditor::Line::truncate(int length) -{ - m_text.resize(length + 1); - m_text.last() = 0; -} bool GTextEditor::write_to_file(const StringView& path) { @@ -1030,9 +941,9 @@ bool GTextEditor::write_to_file(const StringView& path) // Compute the final file size and ftruncate() to make writing fast. // FIXME: Remove this once the kernel is smart enough to do this instead. off_t file_size = 0; - for (int i = 0; i < m_lines.size(); ++i) - file_size += m_lines[i].length(); - file_size += m_lines.size() - 1; + for (int i = 0; i < lines().size(); ++i) + file_size += lines()[i].length(); + file_size += lines().size() - 1; int rc = ftruncate(fd, file_size); if (rc < 0) { @@ -1040,8 +951,8 @@ bool GTextEditor::write_to_file(const StringView& path) return false; } - for (int i = 0; i < m_lines.size(); ++i) { - auto& line = m_lines[i]; + for (int i = 0; i < lines().size(); ++i) { + auto& line = lines()[i]; if (line.length()) { ssize_t nwritten = write(fd, line.characters(), line.length()); if (nwritten < 0) { @@ -1050,7 +961,7 @@ bool GTextEditor::write_to_file(const StringView& path) return false; } } - if (i != m_lines.size() - 1) { + if (i != lines().size() - 1) { char ch = '\n'; ssize_t nwritten = write(fd, &ch, 1); if (nwritten != 1) { @@ -1069,7 +980,7 @@ String GTextEditor::text() const { StringBuilder builder; for (int i = 0; i < line_count(); ++i) { - auto& line = m_lines[i]; + auto& line = lines()[i]; builder.append(line.characters(), line.length()); if (i != line_count() - 1) builder.append('\n'); @@ -1079,8 +990,8 @@ String GTextEditor::text() const void GTextEditor::clear() { - m_lines.clear(); - m_lines.append(make(*this)); + lines().clear(); + lines().append(make(*this)); m_selection.clear(); did_update_selection(); set_cursor(0, 0); @@ -1095,7 +1006,7 @@ String GTextEditor::selected_text() const auto selection = normalized_selection(); StringBuilder builder; for (int i = selection.start().line(); i <= selection.end().line(); ++i) { - auto& line = m_lines[i]; + auto& line = lines()[i]; int selection_start_column_on_line = selection.start().line() == i ? selection.start().column() : 0; int selection_end_column_on_line = selection.end().line() == i ? selection.end().column() : line.length(); builder.append(line.characters() + selection_start_column_on_line, selection_end_column_on_line - selection_start_column_on_line); @@ -1115,13 +1026,13 @@ void GTextEditor::delete_selection() // First delete all the lines in between the first and last one. for (int i = selection.start().line() + 1; i < selection.end().line();) { - m_lines.remove(i); + lines().remove(i); selection.end().set_line(selection.end().line() - 1); } if (selection.start().line() == selection.end().line()) { // Delete within same line. - auto& line = m_lines[selection.start().line()]; + auto& line = lines()[selection.start().line()]; bool whole_line_is_selected = selection.start().column() == 0 && selection.end().column() == line.length(); if (whole_line_is_selected) { line.clear(); @@ -1136,19 +1047,19 @@ void GTextEditor::delete_selection() } else { // Delete across a newline, merging lines. ASSERT(selection.start().line() == selection.end().line() - 1); - auto& first_line = m_lines[selection.start().line()]; - auto& second_line = m_lines[selection.end().line()]; + auto& first_line = lines()[selection.start().line()]; + auto& second_line = lines()[selection.end().line()]; auto before_selection = String(first_line.characters(), first_line.length()).substring(0, selection.start().column()); auto after_selection = String(second_line.characters(), second_line.length()).substring(selection.end().column(), second_line.length() - selection.end().column()); StringBuilder builder(before_selection.length() + after_selection.length()); builder.append(before_selection); builder.append(after_selection); first_line.set_text(builder.to_string()); - m_lines.remove(selection.end().line()); + lines().remove(selection.end().line()); } - if (m_lines.is_empty()) - m_lines.append(make(*this)); + if (lines().is_empty()) + lines().append(make(*this)); m_selection.clear(); did_update_selection(); @@ -1278,7 +1189,7 @@ void GTextEditor::resize_event(GResizeEvent& event) GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap) { - auto& line = m_lines[position.line()]; + auto& line = lines()[position.line()]; if (position.column() == line.length()) { if (position.line() == line_count() - 1) { if (should_wrap == ShouldWrapAtEndOfDocument::Yes) @@ -1295,12 +1206,12 @@ GTextPosition GTextEditor::prev_position_before(const GTextPosition& position, S if (position.column() == 0) { if (position.line() == 0) { if (should_wrap == ShouldWrapAtStartOfDocument::Yes) { - auto& last_line = m_lines[line_count() - 1]; + auto& last_line = lines()[line_count() - 1]; return { line_count() - 1, last_line.length() }; } return {}; } - auto& prev_line = m_lines[position.line() - 1]; + auto& prev_line = lines()[position.line() - 1]; return { position.line() - 1, prev_line.length() }; } return { position.line(), position.column() - 1 }; @@ -1380,7 +1291,7 @@ void GTextEditor::set_selection(const GTextRange& selection) char GTextEditor::character_at(const GTextPosition& position) const { ASSERT(position.line() < line_count()); - auto& line = m_lines[position.line()]; + auto& line = lines()[position.line()]; if (position.column() == line.length()) return '\n'; return line.characters()[position.column()]; @@ -1389,7 +1300,7 @@ char GTextEditor::character_at(const GTextPosition& position) const void GTextEditor::recompute_all_visual_lines() { int y_offset = 0; - for (auto& line : m_lines) { + for (auto& line : lines()) { line.recompute_visual_lines(); line.m_visual_rect.set_y(y_offset); y_offset += line.m_visual_rect.height(); @@ -1398,7 +1309,19 @@ void GTextEditor::recompute_all_visual_lines() update_content_size(); } -void GTextEditor::Line::recompute_visual_lines() +int GTextDocumentLine::visual_line_containing(int column) const +{ + int visual_line_index = 0; + for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) { + if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length())) + return IterationDecision::Break; + ++visual_line_index; + return IterationDecision::Continue; + }); + return visual_line_index; +} + +void GTextDocumentLine::recompute_visual_lines() { m_visual_line_breaks.clear_with_capacity(); @@ -1428,7 +1351,7 @@ void GTextEditor::Line::recompute_visual_lines() } template -void GTextEditor::Line::for_each_visual_line(Callback callback) const +void GTextDocumentLine::for_each_visual_line(Callback callback) const { auto editor_visible_text_rect = m_editor.visible_text_rect_in_inner_coordinates(); int start_of_line = 0; @@ -1464,27 +1387,6 @@ void GTextEditor::set_line_wrapping_enabled(bool enabled) update(); } -int GTextEditor::Line::visual_line_containing(int column) const -{ - int visual_line_index = 0; - for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) { - if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length())) - return IterationDecision::Break; - ++visual_line_index; - return IterationDecision::Continue; - }); - return visual_line_index; -} - -int GTextEditor::Line::first_non_whitespace_column() const -{ - for (int i = 0; i < length(); ++i) { - if (!isspace(m_text[i])) - return i; - } - return length(); -} - void GTextEditor::add_custom_context_menu_action(GAction& action) { m_custom_context_menu_actions.append(action); diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h index 1e9b4649f8..a56e343fda 100644 --- a/Libraries/LibGUI/GTextEditor.h +++ b/Libraries/LibGUI/GTextEditor.h @@ -6,6 +6,7 @@ #include #include #include +#include #include class GAction; @@ -31,6 +32,9 @@ public: }; virtual ~GTextEditor() override; + const GTextDocument& document() const { return *m_document; } + GTextDocument& document() { return *m_document; } + bool is_readonly() const { return m_readonly; } void set_readonly(bool); @@ -56,7 +60,7 @@ public: void set_text(const StringView&); void scroll_cursor_into_view(); void scroll_position_into_view(const GTextPosition&); - int line_count() const { return m_lines.size(); } + int line_count() const { return document().line_count(); } int line_spacing() const { return m_line_spacing; } int line_height() const { return font().glyph_height() + m_line_spacing; } GTextPosition cursor() const { return m_cursor; } @@ -103,17 +107,6 @@ public: void set_cursor(int line, int column); void set_cursor(const GTextPosition&); - struct Span { - GTextRange range; - Color color; - const Font* font { nullptr }; - }; - - void set_spans(const Vector& spans) - { - m_spans = spans; - } - protected: GTextEditor(Type, GWidget* parent); @@ -134,53 +127,24 @@ protected: virtual void resize_event(GResizeEvent&) override; private: + friend class GTextDocumentLine; + void create_actions(); void paint_ruler(Painter&); void update_content_size(); void did_change(); - class Line { - friend class GTextEditor; - - public: - explicit Line(GTextEditor&); - Line(GTextEditor&, const StringView&); - - StringView view() const { return { characters(), length() }; } - const char* characters() const { return m_text.data(); } - int length() const { return m_text.size() - 1; } - void set_text(const StringView&); - void append(char); - void prepend(char); - void insert(int index, char); - void remove(int index); - void append(const char*, int); - void truncate(int length); - void clear(); - void recompute_visual_lines(); - int visual_line_containing(int column) const; - int first_non_whitespace_column() const; - - template - void for_each_visual_line(Callback) const; - - private: - GTextEditor& m_editor; - - // NOTE: This vector is null terminated. - Vector m_text; - - Vector m_visual_line_breaks; - Rect m_visual_rect; - }; - Rect line_content_rect(int item_index) const; Rect line_widget_rect(int line_index) const; Rect cursor_content_rect() const; Rect content_rect_for_position(const GTextPosition&) const; void update_cursor(); - Line& current_line() { return m_lines[m_cursor.line()]; } - const Line& current_line() const { return m_lines[m_cursor.line()]; } + const NonnullOwnPtrVector& lines() const { return document().lines(); } + NonnullOwnPtrVector& lines() { return document().lines(); } + GTextDocumentLine& line(int index) { return document().line(index); } + const GTextDocumentLine& line(int index) const { return document().line(index); } + GTextDocumentLine& current_line() { return line(m_cursor.line()); } + const GTextDocumentLine& current_line() const { return line(m_cursor.line()); } GTextPosition text_position_at(const Point&) const; void insert_at_cursor(char); void insert_at_cursor(const StringView&); @@ -198,7 +162,6 @@ private: Type m_type { MultiLine }; - NonnullOwnPtrVector m_lines; GTextPosition m_cursor; TextAlignment m_text_alignment { TextAlignment::CenterLeft }; bool m_cursor_state { true }; @@ -222,7 +185,7 @@ private: CElapsedTimer m_triple_click_timer; NonnullRefPtrVector m_custom_context_menu_actions; - Vector m_spans; + RefPtr m_document; }; inline const LogStream& operator<<(const LogStream& stream, const GTextPosition& value) diff --git a/Libraries/LibGUI/Makefile b/Libraries/LibGUI/Makefile index 24d3d70ad7..28921c6c7f 100644 --- a/Libraries/LibGUI/Makefile +++ b/Libraries/LibGUI/Makefile @@ -25,6 +25,7 @@ OBJS = \ GVariant.o \ GShortcut.o \ GTextEditor.o \ + GTextDocument.o \ GClipboard.o \ GSortingProxyModel.o \ GStackWidget.o \