LibGUI: Add folding regions to TextDocument

A `TextDocumentFoldingRegion` represents a region of the document which
the user can collapse/expand to make code easier to navigate.
This commit is contained in:
Sam Atkins 2023-02-23 15:25:29 +00:00 committed by Andreas Kling
parent 96cf9355b2
commit 3d25b4eb34
2 changed files with 100 additions and 0 deletions

View file

@ -47,6 +47,7 @@ bool TextDocument::set_text(StringView text, AllowCallback allow_callback)
m_client_notifications_enabled = false;
m_undo_stack.clear();
m_spans.clear();
m_folding_regions.clear();
remove_all_lines();
ArmedScopeGuard clear_text_guard([this]() {
@ -1436,4 +1437,84 @@ void TextDocument::merge_span_collections()
}
}
void TextDocument::set_folding_regions(Vector<TextDocumentFoldingRegion> folding_regions)
{
// Remove any regions that don't span at least 3 lines.
// Currently, we can't do anything useful with them, and our implementation gets very confused by
// single-line regions, so drop them.
folding_regions.remove_all_matching([](TextDocumentFoldingRegion const& region) {
return region.range.line_count() < 3;
});
quick_sort(folding_regions, [](TextDocumentFoldingRegion const& a, TextDocumentFoldingRegion const& b) {
return a.range.start() < b.range.start();
});
for (auto& folding_region : folding_regions) {
folding_region.line_ptr = &line(folding_region.range.start().line());
// Map the new folding region to an old one, to preserve which regions were folded.
// FIXME: This is O(n*n).
for (auto const& existing_folding_region : m_folding_regions) {
// We treat two folding regions as the same if they start on the same TextDocumentLine,
// and have the same line count. The actual line *numbers* might change, but the pointer
// and count should not.
if (existing_folding_region.line_ptr
&& existing_folding_region.line_ptr == folding_region.line_ptr
&& existing_folding_region.range.line_count() == folding_region.range.line_count()) {
folding_region.is_folded = existing_folding_region.is_folded;
break;
}
}
}
// FIXME: Remove any regions that partially overlap another region, since these are invalid.
m_folding_regions = move(folding_regions);
if constexpr (TEXTEDITOR_DEBUG) {
dbgln("TextDocument got {} fold regions:", m_folding_regions.size());
for (auto const& item : m_folding_regions) {
dbgln("- {} (ptr: {:p}, folded: {})", item.range, item.line_ptr, item.is_folded);
}
}
}
Optional<TextDocumentFoldingRegion&> TextDocument::folding_region_starting_on_line(size_t line)
{
return m_folding_regions.first_matching([line](auto& region) {
return region.range.start().line() == line;
});
}
bool TextDocument::line_is_visible(size_t line) const
{
// FIXME: line_is_visible() gets called a lot.
// We could avoid a lot of repeated work if we saved this state on the TextDocumentLine.
return !any_of(m_folding_regions, [line](auto& region) {
return region.is_folded
&& line > region.range.start().line()
&& line < region.range.end().line();
});
}
Vector<TextDocumentFoldingRegion const&> TextDocument::currently_folded_regions() const
{
Vector<TextDocumentFoldingRegion const&> folded_regions;
for (auto& region : m_folding_regions) {
if (region.is_folded) {
// Only add this region if it's not contained within a previous folded region.
// Because regions are sorted by their start position, and regions cannot partially overlap,
// we can just see if it starts inside the last region we appended.
if (!folded_regions.is_empty() && folded_regions.last().range.contains(region.range.start()))
continue;
folded_regions.append(region);
}
}
return folded_regions;
}
}

View file

@ -1,6 +1,7 @@
/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -35,6 +36,13 @@ struct TextDocumentSpan {
bool is_skippable { false };
};
struct TextDocumentFoldingRegion {
TextRange range;
bool is_folded { false };
// This pointer is only used to identify that two TDFRs are the same.
RawPtr<class TextDocumentLine> line_ptr;
};
class TextDocument : public RefCounted<TextDocument> {
public:
enum class SearchShouldWrap {
@ -79,6 +87,16 @@ public:
TextDocumentSpan const* span_at(TextPosition const&) const;
void set_folding_regions(Vector<TextDocumentFoldingRegion>);
bool has_folding_regions() const { return !m_folding_regions.is_empty(); }
Vector<TextDocumentFoldingRegion>& folding_regions() { return m_folding_regions; }
Vector<TextDocumentFoldingRegion> const& folding_regions() const { return m_folding_regions; }
Optional<TextDocumentFoldingRegion&> folding_region_starting_on_line(size_t line);
// Returns all folded FoldingRegions that are not contained inside another folded region.
Vector<TextDocumentFoldingRegion const&> currently_folded_regions() const;
// Returns true if any part of the line is currently visible. (Not inside a folded FoldingRegion.)
bool line_is_visible(size_t line) const;
void append_line(NonnullOwnPtr<TextDocumentLine>);
NonnullOwnPtr<TextDocumentLine> take_line(size_t line_index);
void remove_line(size_t line_index);
@ -148,6 +166,7 @@ private:
NonnullOwnPtrVector<TextDocumentLine> m_lines;
HashMap<u32, Vector<TextDocumentSpan>> m_span_collections;
Vector<TextDocumentSpan> m_spans;
Vector<TextDocumentFoldingRegion> m_folding_regions;
HashTable<Client*> m_clients;
bool m_client_notifications_enabled { true };