Browse Source

LibGUI: Support multiple layers of TextDocument spans

TextDocument::set_spans() now also takes a "span collection index"
argument.

TextDocument keeps a map between a span collection index and its spans.
It merges the spans from all collections into a single set of spans
whenever set_spans() is called.

This allows us to style a document with multiple layers of spans, where
as previously we only supported a single layer of spans that was set
from the SyntaxHighlighter.
Itamar 3 years ago
parent
commit
ab0b4f46f7

+ 98 - 0
Userland/Libraries/LibGUI/TextDocument.cpp

@@ -7,7 +7,9 @@
 
 #include <AK/Badge.h>
 #include <AK/CharacterTypes.h>
+#include <AK/QuickSort.h>
 #include <AK/ScopeGuard.h>
+#include <AK/StdLibExtras.h>
 #include <AK/StringBuilder.h>
 #include <AK/Utf8View.h>
 #include <LibCore/Timer.h>
@@ -982,4 +984,100 @@ void TextDocument::set_unmodified()
     m_undo_stack.set_current_unmodified();
 }
 
+void TextDocument::set_spans(u32 span_collection_index, Vector<TextDocumentSpan> spans)
+{
+    m_span_collections.set(span_collection_index, move(spans));
+    merge_span_collections();
+}
+
+struct SpanAndCollectionIndex {
+    TextDocumentSpan span;
+    u32 collection_index { 0 };
+};
+
+void TextDocument::merge_span_collections()
+{
+    Vector<SpanAndCollectionIndex> sorted_spans;
+    auto collection_indices = m_span_collections.keys();
+    quick_sort(collection_indices);
+
+    for (auto collection_index : collection_indices) {
+        auto spans = m_span_collections.get(collection_index).value();
+        for (auto span : spans) {
+            sorted_spans.append({ move(span), collection_index });
+        }
+    }
+
+    quick_sort(sorted_spans, [](SpanAndCollectionIndex const& a, SpanAndCollectionIndex const& b) {
+        if (a.span.range.start() == b.span.range.start()) {
+            return a.collection_index < b.collection_index;
+        }
+        return a.span.range.start() < b.span.range.start();
+    });
+
+    // The end of the TextRanges of spans are non-inclusive, i.e span range = [X,y).
+    // This transforms the span's range to be inclusive, i.e [X,Y].
+    auto adjust_end = [](GUI::TextDocumentSpan span) -> GUI::TextDocumentSpan {
+        span.range.set_end({ span.range.end().line(), span.range.end().column() == 0 ? 0 : span.range.end().column() - 1 });
+        return span;
+    };
+
+    Vector<SpanAndCollectionIndex> merged_spans;
+    for (auto& span_and_collection_index : sorted_spans) {
+        if (merged_spans.is_empty()) {
+            merged_spans.append(span_and_collection_index);
+            continue;
+        }
+
+        auto const& span = span_and_collection_index.span;
+        auto last_span_and_collection_index = merged_spans.last();
+        auto const& last_span = last_span_and_collection_index.span;
+
+        if (adjust_end(span).range.start() > adjust_end(last_span).range.end()) {
+            // Current span does not intersect with previous one, can simply append to merged list.
+            merged_spans.append(span_and_collection_index);
+            continue;
+        }
+        merged_spans.take_last();
+
+        if (span.range.start() > last_span.range.start()) {
+            SpanAndCollectionIndex first_part = last_span_and_collection_index;
+            first_part.span.range.set_end(span.range.start());
+            merged_spans.append(move(first_part));
+        }
+
+        SpanAndCollectionIndex merged_span;
+        merged_span.collection_index = span_and_collection_index.collection_index;
+        merged_span.span.range = { span.range.start(), min(span.range.end(), last_span.range.end()) };
+        merged_span.span.is_skippable = span.is_skippable | last_span.is_skippable;
+        merged_span.span.data = span.data ? span.data : last_span.data;
+        merged_span.span.attributes.color = span_and_collection_index.collection_index > last_span_and_collection_index.collection_index ? span.attributes.color : last_span.attributes.color;
+        merged_span.span.attributes.bold = span.attributes.bold | last_span.attributes.bold;
+        merged_span.span.attributes.background_color = span.attributes.background_color.has_value() ? span.attributes.background_color.value() : last_span.attributes.background_color;
+        merged_span.span.attributes.underline = span.attributes.underline | last_span.attributes.underline;
+        merged_span.span.attributes.underline_color = span.attributes.underline_color.has_value() ? span.attributes.underline_color.value() : last_span.attributes.underline_color;
+        merged_span.span.attributes.underline_style = span.attributes.underline_style;
+        merged_spans.append(move(merged_span));
+
+        if (span.range.end() == last_span.range.end())
+            continue;
+
+        if (span.range.end() > last_span.range.end()) {
+            SpanAndCollectionIndex last_part = span_and_collection_index;
+            last_part.span.range.set_start(last_span.range.end());
+            merged_spans.append(move(last_part));
+            continue;
+        }
+
+        SpanAndCollectionIndex last_part = last_span_and_collection_index;
+        last_part.span.range.set_start(span.range.end());
+        merged_spans.append(move(last_part));
+    }
+
+    m_spans.clear();
+    for (auto span : merged_spans) {
+        m_spans.append(move(span.span));
+    }
+}
+
 }

+ 4 - 1
Userland/Libraries/LibGUI/TextDocument.h

@@ -62,7 +62,7 @@ public:
     const TextDocumentLine& line(size_t line_index) const { return m_lines[line_index]; }
     TextDocumentLine& line(size_t line_index) { return m_lines[line_index]; }
 
-    void set_spans(Vector<TextDocumentSpan> spans) { m_spans = move(spans); }
+    void set_spans(u32 span_collection_index, Vector<TextDocumentSpan> spans);
 
     bool set_text(StringView, AllowCallback = AllowCallback::Yes);
 
@@ -136,7 +136,10 @@ protected:
     explicit TextDocument(Client* client);
 
 private:
+    void merge_span_collections();
+
     NonnullOwnPtrVector<TextDocumentLine> m_lines;
+    HashMap<u32, Vector<TextDocumentSpan>> m_span_collections;
     Vector<TextDocumentSpan> m_spans;
 
     HashTable<Client*> m_clients;

+ 1 - 1
Userland/Libraries/LibGUI/TextEditor.cpp

@@ -1951,7 +1951,7 @@ void TextEditor::set_syntax_highlighter(OwnPtr<Syntax::Highlighter> highlighter)
         m_highlighter->attach(*this);
         m_needs_rehighlight = true;
     } else
-        document().set_spans({});
+        document().set_spans(Syntax::HighlighterClient::span_collection_index, {});
     if (on_highlighter_change)
         on_highlighter_change();
 }

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

@@ -242,6 +242,8 @@ protected:
     int ruler_width() const;
     int gutter_width() const;
 
+    virtual void highlighter_did_set_spans(Vector<TextDocumentSpan> spans) final { document().set_spans(Syntax::HighlighterClient::span_collection_index, move(spans)); }
+
 private:
     friend class TextDocumentLine;
 

+ 2 - 0
Userland/Libraries/LibSyntax/HighlighterClient.h

@@ -33,6 +33,8 @@ public:
     String get_text() const { return highlighter_did_request_text(); }
     GUI::TextDocument& get_document() { return highlighter_did_request_document(); }
     GUI::TextPosition get_cursor() const { return highlighter_did_request_cursor(); }
+
+    static constexpr auto span_collection_index = 0;
 };
 
 }