Quellcode durchsuchen

LibGUI: Support folding regions in TextEditor

Sam Atkins vor 2 Jahren
Ursprung
Commit
92b128e20a
2 geänderte Dateien mit 136 neuen und 19 gelöschten Zeilen
  1. 131 17
      Userland/Libraries/LibGUI/TextEditor.cpp
  2. 5 2
      Userland/Libraries/LibGUI/TextEditor.h

+ 131 - 17
Userland/Libraries/LibGUI/TextEditor.cpp

@@ -42,6 +42,8 @@ REGISTER_WIDGET(GUI, TextEditor)
 
 namespace GUI {
 
+static constexpr StringView folded_region_summary_text = " ..."sv;
+
 TextEditor::TextEditor(Type type)
     : m_type(type)
 {
@@ -151,16 +153,29 @@ TextPosition TextEditor::text_position_at_content_position(Gfx::IntPoint content
     size_t line_index = 0;
 
     if (position.y() >= 0) {
+        size_t last_visible_line_index = 0;
+        // FIXME: Oh boy is this a slow way of calculating this!
+        // NOTE: Offset by 1 in calculations is because we can't do `i >= 0` with an unsigned type.
+        for (size_t i = line_count(); i > 0; --i) {
+            if (document().line_is_visible(i - 1)) {
+                last_visible_line_index = i - 1;
+                break;
+            }
+        }
+
         for (size_t i = 0; i < line_count(); ++i) {
+            if (!document().line_is_visible(i))
+                continue;
+
             auto& rect = m_line_visual_data[i].visual_rect;
             if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
                 line_index = i;
                 break;
             }
             if (position.y() > rect.bottom())
-                line_index = line_count() - 1;
+                line_index = last_visible_line_index;
         }
-        line_index = max((size_t)0, min(line_index, line_count() - 1));
+        line_index = max((size_t)0, min(line_index, last_visible_line_index));
     }
 
     size_t column_index = 0;
@@ -259,6 +274,23 @@ void TextEditor::mousedown_event(MouseEvent& event)
         return;
     }
 
+    auto text_position = text_position_at(event.position());
+    if (event.modifiers() == 0 && folding_indicator_rect(text_position.line()).contains(event.position())) {
+        if (auto folding_region = document().folding_region_starting_on_line(text_position.line()); folding_region.has_value()) {
+            folding_region->is_folded = !folding_region->is_folded;
+            dbgln_if(TEXTEDITOR_DEBUG, "TextEditor: {} region {}.", folding_region->is_folded ? "Folding"sv : "Unfolding"sv, folding_region->range);
+
+            if (folding_region->is_folded && folding_region->range.contains(cursor())) {
+                // Cursor is now within a hidden range, so move it outside.
+                set_cursor(folding_region->range.start());
+            }
+
+            recompute_all_visual_lines();
+            update();
+            return;
+        }
+    }
+
     if (on_mousedown)
         on_mousedown();
 
@@ -319,6 +351,14 @@ void TextEditor::mousemove_event(MouseEvent& event)
 
     if (m_ruler_visible && ruler_rect_in_inner_coordinates().contains(event.position())) {
         set_override_cursor(Gfx::StandardCursor::None);
+    } else if (m_ruler_visible && folding_indicator_rect_in_inner_coordinates().contains(event.position())) {
+        auto text_position = text_position_at(event.position());
+        if (document().folding_region_starting_on_line(text_position.line()).has_value()
+            && folding_indicator_rect(text_position.line()).contains(event.position())) {
+            set_override_cursor(Gfx::StandardCursor::Hand);
+        } else {
+            set_override_cursor(Gfx::StandardCursor::None);
+        }
     } else if (m_gutter_visible && gutter_rect_in_inner_coordinates().contains(event.position())) {
         set_override_cursor(Gfx::StandardCursor::None);
     } else {
@@ -346,6 +386,11 @@ void TextEditor::automatic_scrolling_timer_did_fire()
     update();
 }
 
+int TextEditor::folding_indicator_width() const
+{
+    return document().has_folding_regions() ? line_height() : 0;
+}
+
 int TextEditor::ruler_width() const
 {
     if (!m_ruler_visible)
@@ -367,6 +412,7 @@ Gfx::IntRect TextEditor::gutter_content_rect(size_t line_index) const
 {
     if (!m_gutter_visible)
         return {};
+
     return {
         0,
         line_content_rect(line_index).y() - vertical_scrollbar().value(),
@@ -379,6 +425,7 @@ Gfx::IntRect TextEditor::ruler_content_rect(size_t line_index) const
 {
     if (!m_ruler_visible)
         return {};
+
     return {
         gutter_width(),
         line_content_rect(line_index).y() - vertical_scrollbar().value(),
@@ -387,6 +434,19 @@ Gfx::IntRect TextEditor::ruler_content_rect(size_t line_index) const
     };
 }
 
+Gfx::IntRect TextEditor::folding_indicator_rect(size_t line_index) const
+{
+    if (!m_ruler_visible || !document().has_folding_regions())
+        return {};
+
+    return {
+        gutter_width() + ruler_width(),
+        line_content_rect(line_index).y() - vertical_scrollbar().value(),
+        folding_indicator_width(),
+        line_content_rect(line_index).height()
+    };
+}
+
 Gfx::IntRect TextEditor::gutter_rect_in_inner_coordinates() const
 {
     return { 0, 0, gutter_width(), widget_inner_rect().height() };
@@ -397,6 +457,11 @@ Gfx::IntRect TextEditor::ruler_rect_in_inner_coordinates() const
     return { gutter_width(), 0, ruler_width(), widget_inner_rect().height() };
 }
 
+Gfx::IntRect TextEditor::folding_indicator_rect_in_inner_coordinates() const
+{
+    return { gutter_width() + ruler_width(), 0, folding_indicator_width(), widget_inner_rect().height() };
+}
+
 Gfx::IntRect TextEditor::visible_text_rect_in_inner_coordinates() const
 {
     return {
@@ -427,6 +492,9 @@ void TextEditor::paint_event(PaintEvent& event)
         unspanned_text_attributes.color = palette().color(is_enabled() ? foreground_role() : Gfx::ColorRole::DisabledText);
     }
 
+    auto& folded_region_summary_font = font().bold_variant();
+    Gfx::TextAttributes folded_region_summary_attributes { palette().color(Gfx::ColorRole::SyntaxComment) };
+
     // NOTE: This lambda and TextEditor::text_width_for_font() are used to substitute all glyphs with m_substitution_code_point if necessary.
     //       Painter::draw_text() and Gfx::Font::width() should not be called directly, but using this lambda and TextEditor::text_width_for_font().
     auto draw_text = [&](Gfx::IntRect const& rect, auto const& raw_text, Gfx::Font const& font, Gfx::TextAlignment alignment, Gfx::TextAttributes attributes, bool substitute = true) {
@@ -469,9 +537,21 @@ void TextEditor::paint_event(PaintEvent& event)
     }
 
     if (m_ruler_visible) {
-        auto ruler_rect = ruler_rect_in_inner_coordinates();
+        auto ruler_rect = ruler_rect_in_inner_coordinates().inflated(0, folding_indicator_width(), 0, 0);
         painter.fill_rect(ruler_rect, palette().ruler());
         painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), palette().ruler_border());
+
+        // Paint +/- buttons for folding regions
+        for (auto const& folding_region : document().folding_regions()) {
+            auto start_line = folding_region.range.start().line();
+            if (!document().line_is_visible(start_line))
+                continue;
+            auto fold_indicator_rect = folding_indicator_rect(start_line).shrunken(4, 4);
+            fold_indicator_rect.set_height(fold_indicator_rect.width());
+            painter.draw_rect(fold_indicator_rect, palette().ruler_inactive_text());
+            auto fold_symbol = folding_region.is_folded ? "+"sv : "-"sv;
+            painter.draw_text(fold_indicator_rect, fold_symbol, font(), Gfx::TextAlignment::Center, palette().ruler_inactive_text());
+        }
     }
 
     size_t first_visible_line = text_position_at(event.rect().top_left()).line();
@@ -482,6 +562,9 @@ void TextEditor::paint_event(PaintEvent& event)
 
     if (m_ruler_visible) {
         for (size_t i = first_visible_line; i <= last_visible_line; ++i) {
+            if (!document().line_is_visible(i))
+                continue;
+
             bool is_current_line = i == m_cursor.line();
             auto ruler_line_rect = ruler_content_rect(i);
             // NOTE: Shrink the rectangle to be only on the first visual line.
@@ -582,9 +665,14 @@ void TextEditor::paint_event(PaintEvent& event)
                     draw_text(span_rect, text, font, m_text_alignment, text_attributes);
                     span_rect.translate_by(span_rect.width(), 0);
                 };
+
+                bool started_new_folded_region = false;
                 while (span_index < document().spans().size()) {
                     auto& span = document().spans()[span_index];
-                    if (span.range.end().line() < line_index) {
+                    // Skip spans that have ended before this point.
+                    // That is, for spans that are for lines inside a folded region.
+                    if ((span.range.end().line() < line_index)
+                        || (span.range.end().line() == line_index && span.range.end().column() <= start_of_visual_line)) {
                         ++span_index;
                         continue;
                     }
@@ -624,9 +712,18 @@ void TextEditor::paint_event(PaintEvent& event)
                     }
                 }
                 // draw unspanned text after last span
-                if (next_column < visual_line_text.length()) {
+                if (!started_new_folded_region && next_column < visual_line_text.length()) {
                     draw_text_helper(next_column, visual_line_text.length(), font(), unspanned_text_attributes);
                 }
+
+                // Paint "..." at the end of the line if it starts a folded region.
+                // FIXME: This doesn't wrap.
+                if (is_last_visual_line) {
+                    if (auto folded_region = document().folding_region_starting_on_line(line_index); folded_region.has_value() && folded_region->is_folded) {
+                        span_rect.set_width(folded_region_summary_font.width(folded_region_summary_text));
+                        draw_text(span_rect, folded_region_summary_text, folded_region_summary_font, m_text_alignment, folded_region_summary_attributes);
+                    }
+                }
             }
 
             if (m_visualize_trailing_whitespace && line.ends_in_whitespace()) {
@@ -1853,8 +1950,10 @@ void TextEditor::recompute_all_visual_lines()
     m_reflow_requested = false;
 
     int y_offset = 0;
+    auto folded_regions = document().currently_folded_regions();
+    auto folded_region_iterator = folded_regions.begin();
     for (size_t line_index = 0; line_index < line_count(); ++line_index) {
-        recompute_visual_lines(line_index);
+        recompute_visual_lines(line_index, folded_region_iterator);
         m_line_visual_data[line_index].visual_rect.set_y(y_offset);
         y_offset += m_line_visual_data[line_index].visual_rect.height();
     }
@@ -1885,7 +1984,7 @@ size_t TextEditor::visual_line_containing(size_t line_index, size_t column) cons
     return visual_line_index;
 }
 
-void TextEditor::recompute_visual_lines(size_t line_index)
+void TextEditor::recompute_visual_lines(size_t line_index, Vector<TextDocumentFoldingRegion const&>::Iterator& folded_region_iterator)
 {
     auto const& line = document().line(line_index);
     size_t line_width_so_far = 0;
@@ -1896,6 +1995,19 @@ void TextEditor::recompute_visual_lines(size_t line_index)
     auto available_width = visible_text_rect_in_inner_coordinates().width();
     auto glyph_spacing = font().glyph_spacing();
 
+    while (!folded_region_iterator.is_end() && folded_region_iterator->range.end() < TextPosition { line_index, 0 })
+        ++folded_region_iterator;
+    bool line_is_visible = true;
+    if (!folded_region_iterator.is_end()) {
+        if (folded_region_iterator->range.start().line() < line_index) {
+            if (folded_region_iterator->range.end().line() > line_index) {
+                line_is_visible = false;
+            } else if (folded_region_iterator->range.end().line() == line_index) {
+                ++folded_region_iterator;
+            }
+        }
+    }
+
     auto wrap_visual_lines_anywhere = [&]() {
         size_t start_of_visual_line = 0;
         for (auto it = line.view().begin(); it != line.view().end(); ++it) {
@@ -1941,16 +2053,18 @@ void TextEditor::recompute_visual_lines(size_t line_index)
         visual_data.visual_lines.append(line.view().substring_view(start_of_visual_line, line.view().length() - start_of_visual_line));
     };
 
-    switch (wrapping_mode()) {
-    case WrappingMode::NoWrap:
-        visual_data.visual_lines.append(line.view());
-        break;
-    case WrappingMode::WrapAnywhere:
-        wrap_visual_lines_anywhere();
-        break;
-    case WrappingMode::WrapAtWords:
-        wrap_visual_lines_at_words();
-        break;
+    if (line_is_visible) {
+        switch (wrapping_mode()) {
+        case WrappingMode::NoWrap:
+            visual_data.visual_lines.append(line.view());
+            break;
+        case WrappingMode::WrapAnywhere:
+            wrap_visual_lines_anywhere();
+            break;
+        case WrappingMode::WrapAtWords:
+            wrap_visual_lines_at_words();
+            break;
+        }
     }
 
     if (is_wrapping_enabled())

+ 5 - 2
Userland/Libraries/LibGUI/TextEditor.h

@@ -269,6 +269,7 @@ protected:
     virtual void cursor_did_change();
     Gfx::IntRect gutter_content_rect(size_t line) const;
     Gfx::IntRect ruler_content_rect(size_t line) const;
+    Gfx::IntRect folding_indicator_rect(size_t line) const;
 
     TextPosition text_position_at(Gfx::IntPoint) const;
     bool ruler_visible() const { return m_ruler_visible; }
@@ -276,7 +277,8 @@ protected:
     Gfx::IntRect content_rect_for_position(TextPosition const&) const;
     int gutter_width() const;
     int ruler_width() const;
-    int fixed_elements_width() const { return gutter_width() + ruler_width(); }
+    int folding_indicator_width() const;
+    int fixed_elements_width() const { return gutter_width() + ruler_width() + folding_indicator_width(); }
 
     virtual void highlighter_did_set_spans(Vector<TextDocumentSpan> spans) final { document().set_spans(Syntax::HighlighterClient::span_collection_index, move(spans)); }
 
@@ -352,13 +354,14 @@ private:
     int content_x_for_position(TextPosition const&) const;
     Gfx::IntRect gutter_rect_in_inner_coordinates() const;
     Gfx::IntRect ruler_rect_in_inner_coordinates() const;
+    Gfx::IntRect folding_indicator_rect_in_inner_coordinates() const;
     Gfx::IntRect visible_text_rect_in_inner_coordinates() const;
     void recompute_all_visual_lines();
     void ensure_cursor_is_valid();
     void rehighlight_if_needed();
 
     size_t visual_line_containing(size_t line_index, size_t column) const;
-    void recompute_visual_lines(size_t line_index);
+    void recompute_visual_lines(size_t line_index, Vector<TextDocumentFoldingRegion const&>::Iterator& folded_region_iterator);
 
     template<class T, class... Args>
     inline void execute(Args&&... args)