LibSyntax+LibJS+LibWeb+LibWebView: Remove unused syntax highlighter code
A future patch will port LibSyntax away from UTF-32. The less code that needs to be ported, the easier that will be.
This commit is contained in:
parent
e6c68e9e91
commit
87cdeda93c
19 changed files with 69 additions and 1002 deletions
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/NeverDestroyed.h>
|
||||
#include <AK/Utf16String.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
#include <LibJS/RustFFI.h>
|
||||
|
|
@ -40,27 +39,11 @@ static Gfx::TextAttributes style_for_token_category(Gfx::Palette const& palette,
|
|||
}
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::is_identifier(u64 token) const
|
||||
{
|
||||
return token_type_from_packed(token) == TokenType::Identifier;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::is_navigatable([[maybe_unused]] u64 token) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
struct RehighlightState {
|
||||
Gfx::Palette const& palette;
|
||||
Vector<Syntax::TextDocumentSpan>& spans;
|
||||
Vector<Syntax::TextDocumentFoldingRegion>& folding_regions;
|
||||
u16 const* source;
|
||||
Syntax::TextPosition position { 0, 0 };
|
||||
|
||||
struct FoldStart {
|
||||
Syntax::TextRange range;
|
||||
};
|
||||
Vector<FoldStart> folding_region_starts;
|
||||
};
|
||||
|
||||
static void advance_position(Syntax::TextPosition& position, u16 const* source, u32 start, u32 len)
|
||||
|
|
@ -106,19 +89,6 @@ static void on_token(void* ctx, FFI::FFIToken const* ffi_token)
|
|||
span.data = pack_token_data(token_type, category);
|
||||
state.spans.append(span);
|
||||
}
|
||||
|
||||
// Track folding regions for {} blocks
|
||||
if (token_type == TokenType::CurlyOpen) {
|
||||
state.folding_region_starts.append({ .range = { token_start, state.position } });
|
||||
} else if (token_type == TokenType::CurlyClose) {
|
||||
if (!state.folding_region_starts.is_empty()) {
|
||||
auto curly_open = state.folding_region_starts.take_last();
|
||||
Syntax::TextDocumentFoldingRegion region;
|
||||
region.range.set_start(curly_open.range.end());
|
||||
region.range.set_end(token_start);
|
||||
state.folding_regions.append(region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::rehighlight(Palette const& palette)
|
||||
|
|
@ -130,43 +100,18 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
auto source_len = source_code->length_in_code_units();
|
||||
|
||||
Vector<Syntax::TextDocumentSpan> spans;
|
||||
Vector<Syntax::TextDocumentFoldingRegion> folding_regions;
|
||||
|
||||
RehighlightState state {
|
||||
.palette = palette,
|
||||
.spans = spans,
|
||||
.folding_regions = folding_regions,
|
||||
.source = source_data,
|
||||
.position = { 0, 0 },
|
||||
.folding_region_starts = {},
|
||||
};
|
||||
|
||||
FFI::rust_tokenize(source_data, source_len, &state,
|
||||
[](void* ctx, FFI::FFIToken const* token) { on_token(ctx, token); });
|
||||
|
||||
m_client->do_set_spans(move(spans));
|
||||
m_client->do_set_folding_regions(move(folding_regions));
|
||||
|
||||
m_has_brace_buddies = false;
|
||||
highlight_matching_token_pair();
|
||||
|
||||
m_client->do_update();
|
||||
}
|
||||
|
||||
Vector<Syntax::Highlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
|
||||
{
|
||||
static NeverDestroyed<Vector<Syntax::Highlighter::MatchingTokenPair>> pairs;
|
||||
if (pairs->is_empty()) {
|
||||
pairs->append({ pack_token_data(TokenType::CurlyOpen, TokenCategory::Punctuation), pack_token_data(TokenType::CurlyClose, TokenCategory::Punctuation) });
|
||||
pairs->append({ pack_token_data(TokenType::ParenOpen, TokenCategory::Punctuation), pack_token_data(TokenType::ParenClose, TokenCategory::Punctuation) });
|
||||
pairs->append({ pack_token_data(TokenType::BracketOpen, TokenCategory::Punctuation), pack_token_data(TokenType::BracketClose, TokenCategory::Punctuation) });
|
||||
}
|
||||
return *pairs;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::token_types_equal(u64 token1, u64 token2) const
|
||||
{
|
||||
return static_cast<TokenType>(token1) == static_cast<TokenType>(token2);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,8 @@ public:
|
|||
SyntaxHighlighter() = default;
|
||||
virtual ~SyntaxHighlighter() override = default;
|
||||
|
||||
virtual bool is_identifier(u64) const override;
|
||||
virtual bool is_navigatable(u64) const override;
|
||||
|
||||
virtual Syntax::Language language() const override { return Syntax::Language::JavaScript; }
|
||||
virtual Optional<StringView> comment_prefix() const override { return "//"sv; }
|
||||
virtual Optional<StringView> comment_suffix() const override { return {}; }
|
||||
virtual void rehighlight(Palette const&) override;
|
||||
|
||||
protected:
|
||||
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
|
||||
virtual bool token_types_equal(u64, u64) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
set(SOURCES
|
||||
Document.cpp
|
||||
Highlighter.cpp
|
||||
Language.cpp
|
||||
)
|
||||
|
||||
ladybird_lib(LibSyntax syntax)
|
||||
|
|
|
|||
|
|
@ -4,74 +4,13 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/CharacterTypes.h>
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/QuickSort.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibSyntax/Document.h>
|
||||
|
||||
namespace Syntax {
|
||||
|
||||
size_t TextDocumentLine::first_non_whitespace_column() const
|
||||
{
|
||||
for (size_t i = 0; i < length(); ++i) {
|
||||
auto code_point = code_points()[i];
|
||||
if (!is_ascii_space(code_point))
|
||||
return i;
|
||||
}
|
||||
return length();
|
||||
}
|
||||
|
||||
Optional<size_t> TextDocumentLine::last_non_whitespace_column() const
|
||||
{
|
||||
for (ssize_t i = length() - 1; i >= 0; --i) {
|
||||
auto code_point = code_points()[i];
|
||||
if (!is_ascii_space(code_point))
|
||||
return i;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool TextDocumentLine::ends_in_whitespace() const
|
||||
{
|
||||
if (!length())
|
||||
return false;
|
||||
return is_ascii_space(code_points()[length() - 1]);
|
||||
}
|
||||
|
||||
bool TextDocumentLine::can_select() const
|
||||
{
|
||||
if (is_empty())
|
||||
return false;
|
||||
for (size_t i = 0; i < length(); ++i) {
|
||||
auto code_point = code_points()[i];
|
||||
if (code_point != '\n' && code_point != '\r' && code_point != '\f' && code_point != '\v')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t TextDocumentLine::leading_spaces() const
|
||||
{
|
||||
size_t count = 0;
|
||||
for (; count < m_text.size(); ++count) {
|
||||
if (m_text[count] != ' ') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
ByteString TextDocumentLine::to_utf8() const
|
||||
{
|
||||
StringBuilder builder;
|
||||
builder.append(view());
|
||||
return builder.to_byte_string();
|
||||
}
|
||||
|
||||
TextDocumentLine::TextDocumentLine(Document& document)
|
||||
{
|
||||
clear(document);
|
||||
|
|
@ -88,12 +27,6 @@ void TextDocumentLine::clear(Document& document)
|
|||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::set_text(Document& document, Vector<u32> const text)
|
||||
{
|
||||
m_text = move(text);
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
bool TextDocumentLine::set_text(Document& document, StringView text)
|
||||
{
|
||||
if (text.is_empty()) {
|
||||
|
|
@ -111,115 +44,10 @@ bool TextDocumentLine::set_text(Document& document, StringView text)
|
|||
return true;
|
||||
}
|
||||
|
||||
void TextDocumentLine::append(Document& document, u32 const* code_points, size_t length)
|
||||
void Document::set_spans(Vector<TextDocumentSpan> spans)
|
||||
{
|
||||
if (length == 0)
|
||||
return;
|
||||
m_text.append(code_points, length);
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::append(Document& document, u32 code_point)
|
||||
{
|
||||
insert(document, length(), code_point);
|
||||
}
|
||||
|
||||
void TextDocumentLine::prepend(Document& document, u32 code_point)
|
||||
{
|
||||
insert(document, 0, code_point);
|
||||
}
|
||||
|
||||
void TextDocumentLine::insert(Document& document, size_t index, u32 code_point)
|
||||
{
|
||||
if (index == length()) {
|
||||
m_text.append(code_point);
|
||||
} else {
|
||||
m_text.insert(index, code_point);
|
||||
}
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::remove(Document& document, size_t index)
|
||||
{
|
||||
if (index == length()) {
|
||||
m_text.take_last();
|
||||
} else {
|
||||
m_text.remove(index);
|
||||
}
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::remove_range(Document& document, size_t start, size_t length)
|
||||
{
|
||||
VERIFY(length <= m_text.size());
|
||||
|
||||
Vector<u32> new_data;
|
||||
new_data.ensure_capacity(m_text.size() - length);
|
||||
for (size_t i = 0; i < start; ++i)
|
||||
new_data.append(m_text[i]);
|
||||
for (size_t i = (start + length); i < m_text.size(); ++i)
|
||||
new_data.append(m_text[i]);
|
||||
m_text = move(new_data);
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::keep_range(Document& document, size_t start_index, size_t length)
|
||||
{
|
||||
VERIFY(start_index + length < m_text.size());
|
||||
|
||||
Vector<u32> new_data;
|
||||
new_data.ensure_capacity(m_text.size());
|
||||
for (size_t i = start_index; i <= (start_index + length); i++)
|
||||
new_data.append(m_text[i]);
|
||||
|
||||
m_text = move(new_data);
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
void TextDocumentLine::truncate(Document& document, size_t length)
|
||||
{
|
||||
m_text.resize(length);
|
||||
document.update_views({});
|
||||
}
|
||||
|
||||
TextDocumentSpan const* Document::span_at(TextPosition const& position) const
|
||||
{
|
||||
for (auto& span : m_spans) {
|
||||
if (span.range.contains(position))
|
||||
return &span;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Document::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 Document::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();
|
||||
quick_sort(spans, [](TextDocumentSpan const& a, TextDocumentSpan const& b) {
|
||||
return a.range.start() < b.range.start();
|
||||
});
|
||||
|
||||
// The end of the TextRanges of spans are non-inclusive, i.e span range = [X,y).
|
||||
|
|
@ -229,54 +57,51 @@ void Document::merge_span_collections()
|
|||
return span;
|
||||
};
|
||||
|
||||
Vector<SpanAndCollectionIndex> merged_spans;
|
||||
for (auto& span_and_collection_index : sorted_spans) {
|
||||
Vector<TextDocumentSpan> merged_spans;
|
||||
for (auto& span : spans) {
|
||||
if (merged_spans.is_empty()) {
|
||||
merged_spans.append(span_and_collection_index);
|
||||
merged_spans.append(span);
|
||||
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;
|
||||
auto last_span = merged_spans.last();
|
||||
|
||||
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);
|
||||
merged_spans.append(span);
|
||||
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());
|
||||
auto first_part = last_span;
|
||||
first_part.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_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.has_value() ? span.attributes.underline_style : last_span.attributes.underline_style;
|
||||
TextDocumentSpan merged_span;
|
||||
merged_span.range = { span.range.start(), min(span.range.end(), last_span.range.end()) };
|
||||
merged_span.is_skippable = span.is_skippable | last_span.is_skippable;
|
||||
merged_span.data = span.data ? span.data : last_span.data;
|
||||
merged_span.attributes.color = last_span.attributes.color;
|
||||
merged_span.attributes.bold = span.attributes.bold | last_span.attributes.bold;
|
||||
merged_span.attributes.background_color = span.attributes.background_color.has_value() ? span.attributes.background_color.value() : last_span.attributes.background_color;
|
||||
merged_span.attributes.underline_color = span.attributes.underline_color.has_value() ? span.attributes.underline_color.value() : last_span.attributes.underline_color;
|
||||
merged_span.attributes.underline_style = span.attributes.underline_style.has_value() ? span.attributes.underline_style : last_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());
|
||||
auto last_part = span;
|
||||
last_part.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());
|
||||
auto last_part = last_span;
|
||||
last_part.range.set_start(span.range.end());
|
||||
merged_spans.append(move(last_part));
|
||||
}
|
||||
|
||||
|
|
@ -284,106 +109,26 @@ void Document::merge_span_collections()
|
|||
TextDocumentSpan previous_span { .range = { TextPosition(0, 0), TextPosition(0, 0) }, .attributes = {} };
|
||||
for (auto span : merged_spans) {
|
||||
// Validate spans
|
||||
if (!span.span.range.is_valid()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Invalid span {} => ignoring", span.span.range);
|
||||
if (!span.range.is_valid()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Invalid span {} => ignoring", span.range);
|
||||
continue;
|
||||
}
|
||||
if (span.span.range.end() < span.span.range.start()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Span {} has negative length => ignoring", span.span.range);
|
||||
if (span.range.end() < span.range.start()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Span {} has negative length => ignoring", span.range);
|
||||
continue;
|
||||
}
|
||||
if (span.span.range.end() < previous_span.range.start()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Spans not sorted (Span {} ends before previous span {}) => ignoring", span.span.range, previous_span.range);
|
||||
if (span.range.end() < previous_span.range.start()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Spans not sorted (Span {} ends before previous span {}) => ignoring", span.range, previous_span.range);
|
||||
continue;
|
||||
}
|
||||
if (span.span.range.start() < previous_span.range.end()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Span {} overlaps previous span {} => ignoring", span.span.range, previous_span.range);
|
||||
if (span.range.start() < previous_span.range.end()) {
|
||||
dbgln_if(TEXTEDITOR_DEBUG, "Span {} overlaps previous span {} => ignoring", span.range, previous_span.range);
|
||||
continue;
|
||||
}
|
||||
|
||||
previous_span = span.span;
|
||||
m_spans.append(move(span.span));
|
||||
previous_span = span;
|
||||
m_spans.append(move(span));
|
||||
}
|
||||
}
|
||||
|
||||
void Document::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("Document 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&> Document::folding_region_starting_on_line(size_t line)
|
||||
{
|
||||
return m_folding_regions.first_matching([line](auto& region) {
|
||||
return region.range.start().line() == line;
|
||||
});
|
||||
}
|
||||
|
||||
bool Document::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&> Document::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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Utf32View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/TextAttributes.h>
|
||||
#include <LibSyntax/Forward.h>
|
||||
#include <LibSyntax/TextRange.h>
|
||||
|
|
@ -22,41 +22,17 @@ 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 TextDocumentLine {
|
||||
public:
|
||||
explicit TextDocumentLine(Document&);
|
||||
explicit TextDocumentLine(Document&, StringView);
|
||||
|
||||
ByteString to_utf8() const;
|
||||
|
||||
Utf32View view() const LIFETIME_BOUND { return { code_points(), length() }; }
|
||||
u32 const* code_points() const { return m_text.data(); }
|
||||
bool is_empty() const { return length() == 0; }
|
||||
size_t length() const { return m_text.size(); }
|
||||
bool set_text(Document&, StringView);
|
||||
void set_text(Document&, Vector<u32>);
|
||||
void append(Document&, u32);
|
||||
void prepend(Document&, u32);
|
||||
void insert(Document&, size_t index, u32);
|
||||
void remove(Document&, size_t index);
|
||||
void append(Document&, u32 const*, size_t);
|
||||
void truncate(Document&, size_t length);
|
||||
void clear(Document&);
|
||||
void remove_range(Document&, size_t start, size_t length);
|
||||
void keep_range(Document&, size_t start_index, size_t end_index);
|
||||
|
||||
size_t first_non_whitespace_column() const;
|
||||
Optional<size_t> last_non_whitespace_column() const;
|
||||
bool ends_in_whitespace() const;
|
||||
bool can_select() const;
|
||||
bool is_empty() const { return length() == 0; }
|
||||
size_t leading_spaces() const;
|
||||
|
||||
private:
|
||||
// NOTE: This vector is null terminated.
|
||||
|
|
@ -68,22 +44,8 @@ public:
|
|||
Document() = default;
|
||||
virtual ~Document() = default;
|
||||
|
||||
void set_spans(u32 span_collection_index, Vector<TextDocumentSpan> spans);
|
||||
bool has_spans() const { return !m_spans.is_empty(); }
|
||||
void set_spans(Vector<TextDocumentSpan> spans);
|
||||
Vector<TextDocumentSpan>& spans() { return m_spans; }
|
||||
Vector<TextDocumentSpan> const& spans() const { return m_spans; }
|
||||
void set_span_at_index(size_t index, TextDocumentSpan span) { m_spans[index] = move(span); }
|
||||
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;
|
||||
|
||||
virtual TextDocumentLine const& line(size_t line_index) const = 0;
|
||||
virtual TextDocumentLine& line(size_t line_index) = 0;
|
||||
|
|
@ -91,12 +53,7 @@ public:
|
|||
virtual void update_views(Badge<TextDocumentLine>) = 0;
|
||||
|
||||
protected:
|
||||
HashMap<u32, Vector<TextDocumentSpan>> m_span_collections;
|
||||
Vector<TextDocumentSpan> m_spans;
|
||||
Vector<TextDocumentFoldingRegion> m_folding_regions;
|
||||
|
||||
private:
|
||||
void merge_span_collections();
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,99 +4,10 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibSyntax/Highlighter.h>
|
||||
|
||||
namespace Syntax {
|
||||
|
||||
void Highlighter::highlight_matching_token_pair()
|
||||
{
|
||||
auto& document = m_client->get_document();
|
||||
|
||||
enum class Direction {
|
||||
Forward,
|
||||
Backward,
|
||||
};
|
||||
|
||||
auto find_span_of_type = [&](auto i, u64 type, u64 not_type, Direction direction) -> Optional<size_t> {
|
||||
size_t nesting_level = 0;
|
||||
bool forward = direction == Direction::Forward;
|
||||
|
||||
if (forward) {
|
||||
++i;
|
||||
if (i >= document.spans().size())
|
||||
return {};
|
||||
} else {
|
||||
if (i == 0)
|
||||
return {};
|
||||
--i;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
auto& span = document.spans().at(i);
|
||||
auto span_token_type = span.data;
|
||||
if (token_types_equal(span_token_type, not_type)) {
|
||||
++nesting_level;
|
||||
} else if (token_types_equal(span_token_type, type)) {
|
||||
if (nesting_level-- <= 0)
|
||||
return i;
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
++i;
|
||||
if (i >= document.spans().size())
|
||||
return {};
|
||||
} else {
|
||||
if (i == 0)
|
||||
return {};
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
auto make_buddies = [&](int index0, int index1) {
|
||||
auto& buddy0 = document.spans()[index0];
|
||||
auto& buddy1 = document.spans()[index1];
|
||||
m_has_brace_buddies = true;
|
||||
m_brace_buddies[0].index = index0;
|
||||
m_brace_buddies[1].index = index1;
|
||||
m_brace_buddies[0].span_backup = buddy0;
|
||||
m_brace_buddies[1].span_backup = buddy1;
|
||||
buddy0.attributes.background_color = Color::DarkCyan;
|
||||
buddy1.attributes.background_color = Color::DarkCyan;
|
||||
buddy0.attributes.color = Color::White;
|
||||
buddy1.attributes.color = Color::White;
|
||||
m_client->do_update();
|
||||
};
|
||||
|
||||
auto pairs = matching_token_pairs();
|
||||
|
||||
for (size_t i = 0; i < document.spans().size(); ++i) {
|
||||
auto& span = const_cast<TextDocumentSpan&>(document.spans().at(i));
|
||||
auto token_type = span.data;
|
||||
|
||||
for (auto& pair : pairs) {
|
||||
if (token_types_equal(token_type, pair.open) && span.range.start() == m_client->get_cursor()) {
|
||||
auto buddy = find_span_of_type(i, pair.close, pair.open, Direction::Forward);
|
||||
if (buddy.has_value())
|
||||
make_buddies(i, buddy.value());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& pair : pairs) {
|
||||
if (token_types_equal(token_type, pair.close) && span.range.end() == m_client->get_cursor()) {
|
||||
auto buddy = find_span_of_type(i, pair.open, pair.close, Direction::Backward);
|
||||
if (buddy.has_value())
|
||||
make_buddies(i, buddy.value());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Highlighter::attach(HighlighterClient& client)
|
||||
{
|
||||
VERIFY(!m_client);
|
||||
|
|
@ -108,33 +19,4 @@ void Highlighter::detach()
|
|||
m_client = nullptr;
|
||||
}
|
||||
|
||||
void Highlighter::cursor_did_change()
|
||||
{
|
||||
auto& document = m_client->get_document();
|
||||
if (m_has_brace_buddies) {
|
||||
if (m_brace_buddies[0].index >= 0 && m_brace_buddies[0].index < static_cast<int>(document.spans().size()))
|
||||
document.set_span_at_index(m_brace_buddies[0].index, m_brace_buddies[0].span_backup);
|
||||
if (m_brace_buddies[1].index >= 0 && m_brace_buddies[1].index < static_cast<int>(document.spans().size()))
|
||||
document.set_span_at_index(m_brace_buddies[1].index, m_brace_buddies[1].span_backup);
|
||||
m_has_brace_buddies = false;
|
||||
m_client->do_update();
|
||||
}
|
||||
highlight_matching_token_pair();
|
||||
}
|
||||
|
||||
Vector<Highlighter::MatchingTokenPair> Highlighter::matching_token_pairs() const
|
||||
{
|
||||
auto own_pairs = matching_token_pairs_impl();
|
||||
own_pairs.ensure_capacity(own_pairs.size() + m_nested_token_pairs.size());
|
||||
for (auto& nested_pair : m_nested_token_pairs)
|
||||
own_pairs.append(nested_pair);
|
||||
return own_pairs;
|
||||
}
|
||||
|
||||
void Highlighter::register_nested_token_pairs(Vector<MatchingTokenPair> pairs)
|
||||
{
|
||||
for (auto& pair : pairs)
|
||||
m_nested_token_pairs.set(pair);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
#include <LibSyntax/Document.h>
|
||||
#include <LibSyntax/HighlighterClient.h>
|
||||
|
|
@ -23,57 +22,25 @@ public:
|
|||
virtual ~Highlighter() = default;
|
||||
|
||||
virtual Language language() const = 0;
|
||||
virtual Optional<StringView> comment_prefix() const = 0;
|
||||
virtual Optional<StringView> comment_suffix() const = 0;
|
||||
virtual void rehighlight(Palette const&) = 0;
|
||||
virtual void highlight_matching_token_pair();
|
||||
|
||||
virtual bool is_identifier(u64) const { return false; }
|
||||
virtual bool is_navigatable(u64) const { return false; }
|
||||
|
||||
void attach(HighlighterClient&);
|
||||
void detach();
|
||||
void cursor_did_change();
|
||||
|
||||
struct MatchingTokenPair {
|
||||
u64 open;
|
||||
u64 close;
|
||||
};
|
||||
Vector<MatchingTokenPair> matching_token_pairs() const;
|
||||
|
||||
template<typename T>
|
||||
bool fast_is() const = delete;
|
||||
|
||||
// FIXME: When other syntax highlighters start using a language server, we should add a common base class here.
|
||||
virtual bool is_cpp_semantic_highlighter() const { return false; }
|
||||
|
||||
protected:
|
||||
Highlighter() = default;
|
||||
|
||||
// FIXME: This should be WeakPtr somehow
|
||||
HighlighterClient* m_client { nullptr };
|
||||
|
||||
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const = 0;
|
||||
virtual bool token_types_equal(u64, u64) const = 0;
|
||||
void register_nested_token_pairs(Vector<MatchingTokenPair>);
|
||||
void clear_nested_token_pairs() { m_nested_token_pairs.clear(); }
|
||||
size_t first_free_token_kind_serial_value() const { return m_nested_token_pairs.size(); }
|
||||
|
||||
struct BuddySpan {
|
||||
int index { -1 };
|
||||
TextDocumentSpan span_backup;
|
||||
};
|
||||
|
||||
bool m_has_brace_buddies { false };
|
||||
BuddySpan m_brace_buddies[2];
|
||||
HashTable<MatchingTokenPair> m_nested_token_pairs;
|
||||
};
|
||||
|
||||
class ProxyHighlighterClient final : public Syntax::HighlighterClient {
|
||||
public:
|
||||
ProxyHighlighterClient(Syntax::HighlighterClient& client, TextPosition start, u64 nested_kind_start_value, StringView source)
|
||||
: m_document(client.get_document())
|
||||
, m_text(source)
|
||||
ProxyHighlighterClient(TextPosition start, u64 nested_kind_start_value, StringView source)
|
||||
: m_text(source)
|
||||
, m_start(start)
|
||||
, m_nested_kind_start_value(nested_kind_start_value)
|
||||
{
|
||||
|
|
@ -98,64 +65,14 @@ public:
|
|||
return spans;
|
||||
}
|
||||
|
||||
Vector<TextDocumentFoldingRegion> corrected_folding_regions() const
|
||||
{
|
||||
Vector<TextDocumentFoldingRegion> folding_regions { m_folding_regions };
|
||||
for (auto& entry : folding_regions) {
|
||||
entry.range.start() = {
|
||||
entry.range.start().line() + m_start.line(),
|
||||
entry.range.start().line() == 0 ? entry.range.start().column() + m_start.column() : entry.range.start().column(),
|
||||
};
|
||||
entry.range.end() = {
|
||||
entry.range.end().line() + m_start.line(),
|
||||
entry.range.end().line() == 0 ? entry.range.end().column() + m_start.column() : entry.range.end().column(),
|
||||
};
|
||||
}
|
||||
|
||||
return folding_regions;
|
||||
}
|
||||
|
||||
Vector<Syntax::Highlighter::MatchingTokenPair> corrected_token_pairs(Vector<Syntax::Highlighter::MatchingTokenPair> pairs) const
|
||||
{
|
||||
for (auto& pair : pairs) {
|
||||
pair.close += m_nested_kind_start_value;
|
||||
pair.open += m_nested_kind_start_value;
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private:
|
||||
virtual Vector<TextDocumentSpan> const& spans() const override { return m_spans; }
|
||||
virtual void set_span_at_index(size_t index, TextDocumentSpan span) override { m_spans.at(index) = move(span); }
|
||||
|
||||
virtual Vector<TextDocumentFoldingRegion>& folding_regions() override { return m_folding_regions; }
|
||||
virtual Vector<TextDocumentFoldingRegion> const& folding_regions() const override { return m_folding_regions; }
|
||||
|
||||
virtual ByteString highlighter_did_request_text() const override { return m_text; }
|
||||
virtual void highlighter_did_request_update() override { }
|
||||
virtual Document& highlighter_did_request_document() override { return m_document; }
|
||||
virtual TextPosition highlighter_did_request_cursor() const override { return {}; }
|
||||
virtual StringView highlighter_did_request_text() const override { return m_text; }
|
||||
virtual void highlighter_did_set_spans(Vector<TextDocumentSpan> spans) override { m_spans = move(spans); }
|
||||
virtual void highlighter_did_set_folding_regions(Vector<TextDocumentFoldingRegion> folding_regions) override { m_folding_regions = folding_regions; }
|
||||
|
||||
Vector<TextDocumentSpan> m_spans;
|
||||
Vector<TextDocumentFoldingRegion> m_folding_regions;
|
||||
Document& m_document;
|
||||
StringView m_text;
|
||||
TextPosition m_start;
|
||||
u64 m_nested_kind_start_value { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Traits<Syntax::Highlighter::MatchingTokenPair> : public AK::DefaultTraits<Syntax::Highlighter::MatchingTokenPair> {
|
||||
static unsigned hash(Syntax::Highlighter::MatchingTokenPair const& pair)
|
||||
{
|
||||
return pair_int_hash(u64_hash(pair.open), u64_hash(pair.close));
|
||||
}
|
||||
static bool equals(Syntax::Highlighter::MatchingTokenPair const& a, Syntax::Highlighter::MatchingTokenPair const& b)
|
||||
{
|
||||
return a.open == b.open && a.close == b.close;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibSyntax/Document.h>
|
||||
#include <LibSyntax/TextPosition.h>
|
||||
|
||||
namespace Syntax {
|
||||
|
||||
|
|
@ -18,29 +17,12 @@ class HighlighterClient {
|
|||
public:
|
||||
virtual ~HighlighterClient() = default;
|
||||
|
||||
virtual Vector<TextDocumentSpan> const& spans() const = 0;
|
||||
virtual void set_span_at_index(size_t index, TextDocumentSpan span) = 0;
|
||||
virtual void clear_spans() { do_set_spans({}); }
|
||||
|
||||
virtual Vector<TextDocumentFoldingRegion>& folding_regions() = 0;
|
||||
virtual Vector<TextDocumentFoldingRegion> const& folding_regions() const = 0;
|
||||
|
||||
virtual ByteString highlighter_did_request_text() const = 0;
|
||||
virtual void highlighter_did_request_update() = 0;
|
||||
virtual Document& highlighter_did_request_document() = 0;
|
||||
virtual TextPosition highlighter_did_request_cursor() const = 0;
|
||||
virtual StringView highlighter_did_request_text() const = 0;
|
||||
virtual void highlighter_did_set_spans(Vector<TextDocumentSpan>) = 0;
|
||||
virtual void highlighter_did_set_folding_regions(Vector<TextDocumentFoldingRegion>) = 0;
|
||||
|
||||
void do_set_spans(Vector<TextDocumentSpan> spans) { highlighter_did_set_spans(move(spans)); }
|
||||
void do_set_folding_regions(Vector<TextDocumentFoldingRegion> folding_regions) { highlighter_did_set_folding_regions(move(folding_regions)); }
|
||||
void do_update() { highlighter_did_request_update(); }
|
||||
|
||||
ByteString get_text() const { return highlighter_did_request_text(); }
|
||||
Document& get_document() { return highlighter_did_request_document(); }
|
||||
TextPosition get_cursor() const { return highlighter_did_request_cursor(); }
|
||||
|
||||
static constexpr auto span_collection_index = 0;
|
||||
StringView get_text() const { return highlighter_did_request_text(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2023, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Language.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibSyntax/Highlighter.h>
|
||||
|
||||
namespace Syntax {
|
||||
|
||||
StringView language_to_string(Language language)
|
||||
{
|
||||
switch (language) {
|
||||
case Language::CMake:
|
||||
return "CMake"sv;
|
||||
case Language::CMakeCache:
|
||||
return "CMakeCache"sv;
|
||||
case Language::Cpp:
|
||||
return "C++"sv;
|
||||
case Language::CSS:
|
||||
return "CSS"sv;
|
||||
case Language::GitCommit:
|
||||
return "Git"sv;
|
||||
case Language::GML:
|
||||
return "GML"sv;
|
||||
case Language::HTML:
|
||||
return "HTML"sv;
|
||||
case Language::INI:
|
||||
return "INI"sv;
|
||||
case Language::JavaScript:
|
||||
return "JavaScript"sv;
|
||||
case Language::Markdown:
|
||||
return "Markdown"sv;
|
||||
case Language::PlainText:
|
||||
return "Plain Text"sv;
|
||||
case Language::Shell:
|
||||
return "Shell"sv;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
StringView common_language_extension(Language language)
|
||||
{
|
||||
switch (language) {
|
||||
case Language::CMake:
|
||||
return "cmake"sv;
|
||||
case Language::CMakeCache:
|
||||
return {};
|
||||
case Language::Cpp:
|
||||
return "cpp"sv;
|
||||
case Language::CSS:
|
||||
return "css"sv;
|
||||
case Language::GitCommit:
|
||||
return {};
|
||||
case Language::GML:
|
||||
return "gml"sv;
|
||||
case Language::HTML:
|
||||
return "html"sv;
|
||||
case Language::INI:
|
||||
return "ini"sv;
|
||||
case Language::JavaScript:
|
||||
return "js"sv;
|
||||
case Language::Markdown:
|
||||
return "md"sv;
|
||||
case Language::PlainText:
|
||||
return "txt"sv;
|
||||
case Language::Shell:
|
||||
return "sh"sv;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
Optional<Language> language_from_name(StringView name)
|
||||
{
|
||||
if (name.equals_ignoring_ascii_case("CMake"sv))
|
||||
return Language::CMake;
|
||||
if (name.equals_ignoring_ascii_case("CMakeCache"sv))
|
||||
return Language::CMakeCache;
|
||||
if (name.equals_ignoring_ascii_case("Cpp"sv))
|
||||
return Language::Cpp;
|
||||
if (name.equals_ignoring_ascii_case("CSS"sv))
|
||||
return Language::CSS;
|
||||
if (name.equals_ignoring_ascii_case("GitCommit"sv))
|
||||
return Language::GitCommit;
|
||||
if (name.equals_ignoring_ascii_case("GML"sv))
|
||||
return Language::GML;
|
||||
if (name.equals_ignoring_ascii_case("HTML"sv))
|
||||
return Language::HTML;
|
||||
if (name.equals_ignoring_ascii_case("INI"sv))
|
||||
return Language::INI;
|
||||
if (name.equals_ignoring_ascii_case("JavaScript"sv))
|
||||
return Language::JavaScript;
|
||||
if (name.equals_ignoring_ascii_case("Markdown"sv))
|
||||
return Language::Markdown;
|
||||
if (name.equals_ignoring_ascii_case("PlainText"sv))
|
||||
return Language::PlainText;
|
||||
if (name.equals_ignoring_ascii_case("Shell"sv))
|
||||
return Language::Shell;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Optional<Language> language_from_filename(LexicalPath const& file)
|
||||
{
|
||||
if (file.title() == "COMMIT_EDITMSG"sv)
|
||||
return Language::GitCommit;
|
||||
|
||||
auto extension = file.extension();
|
||||
VERIFY(!extension.starts_with('.'));
|
||||
if (extension == "cmake"sv || (extension == "txt"sv && file.title() == "CMakeLists"sv))
|
||||
return Language::CMake;
|
||||
if (extension == "txt"sv && file.title() == "CMakeCache"sv)
|
||||
return Language::CMakeCache;
|
||||
if (extension.is_one_of("c"sv, "cc"sv, "cxx"sv, "cpp"sv, "c++", "h"sv, "hh"sv, "hxx"sv, "hpp"sv, "h++"sv))
|
||||
return Language::Cpp;
|
||||
if (extension == "css"sv)
|
||||
return Language::CSS;
|
||||
if (extension == "gml"sv)
|
||||
return Language::GML;
|
||||
if (extension.is_one_of("html"sv, "htm"sv))
|
||||
return Language::HTML;
|
||||
if (extension.is_one_of("ini"sv, "af"sv))
|
||||
return Language::INI;
|
||||
if (extension.is_one_of("js"sv, "mjs"sv, "json"sv))
|
||||
return Language::JavaScript;
|
||||
if (extension == "md"sv)
|
||||
return Language::Markdown;
|
||||
if (extension.is_one_of("sh"sv, "bash"sv))
|
||||
return Language::Shell;
|
||||
|
||||
// Check "txt" after the CMake related files that use "txt" as their extension.
|
||||
if (extension == "txt"sv)
|
||||
return Language::PlainText;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,29 +6,14 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Types.h>
|
||||
|
||||
namespace Syntax {
|
||||
|
||||
enum class Language {
|
||||
CMake,
|
||||
CMakeCache,
|
||||
Cpp,
|
||||
enum class Language : u8 {
|
||||
CSS,
|
||||
GitCommit,
|
||||
GML,
|
||||
HTML,
|
||||
INI,
|
||||
JavaScript,
|
||||
Markdown,
|
||||
PlainText,
|
||||
Shell,
|
||||
};
|
||||
|
||||
StringView language_to_string(Language);
|
||||
StringView common_language_extension(Language);
|
||||
Optional<Language> language_from_name(StringView);
|
||||
Optional<Language> language_from_filename(LexicalPath const&);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,50 +21,18 @@ public:
|
|||
}
|
||||
|
||||
bool is_valid() const { return m_start.is_valid() && m_end.is_valid() && m_start != m_end; }
|
||||
void clear()
|
||||
{
|
||||
m_start = {};
|
||||
m_end = {};
|
||||
}
|
||||
|
||||
TextPosition& start() { return m_start; }
|
||||
TextPosition& end() { return m_end; }
|
||||
TextPosition const& start() const { return m_start; }
|
||||
TextPosition const& end() const { return m_end; }
|
||||
|
||||
size_t line_count() const { return normalized_end().line() - normalized_start().line() + 1; }
|
||||
|
||||
TextRange normalized() const { return TextRange(normalized_start(), normalized_end()); }
|
||||
|
||||
void set_start(TextPosition const& position) { m_start = position; }
|
||||
void set_end(TextPosition const& position) { m_end = position; }
|
||||
|
||||
void set(TextPosition const& start, TextPosition const& end)
|
||||
{
|
||||
m_start = start;
|
||||
m_end = end;
|
||||
}
|
||||
|
||||
bool operator==(TextRange const& other) const
|
||||
{
|
||||
return m_start == other.m_start && m_end == other.m_end;
|
||||
}
|
||||
|
||||
bool contains(TextPosition const& position) const
|
||||
{
|
||||
if (position.line() <= m_start.line() && (position.line() != m_start.line() || position.column() < m_start.column()))
|
||||
return false;
|
||||
if (position.line() >= m_end.line() && (position.line() != m_end.line() || position.column() > m_end.column()))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
TextPosition normalized_start() const { return m_start < m_end ? m_start : m_end; }
|
||||
TextPosition normalized_end() const { return m_start < m_end ? m_end : m_start; }
|
||||
|
||||
TextPosition m_start {};
|
||||
TextPosition m_end {};
|
||||
TextPosition m_start;
|
||||
TextPosition m_end;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,29 +5,16 @@
|
|||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/NeverDestroyed.h>
|
||||
#include <LibWeb/CSS/Parser/Tokenizer.h>
|
||||
#include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
bool SyntaxHighlighter::is_identifier(u64 token) const
|
||||
{
|
||||
return static_cast<CSS::Parser::Token::Type>(token) == CSS::Parser::Token::Type::Ident;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::is_navigatable(u64) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::rehighlight(Palette const& palette)
|
||||
{
|
||||
dbgln_if(SYNTAX_HIGHLIGHTING_DEBUG, "(CSS::SyntaxHighlighter) starting rehighlight");
|
||||
auto text = m_client->get_text();
|
||||
|
||||
Vector<Parser::Token> folding_region_start_tokens;
|
||||
Vector<Syntax::TextDocumentFoldingRegion> folding_regions;
|
||||
Vector<Syntax::TextDocumentSpan> spans;
|
||||
|
||||
auto highlight = [&](auto start_line, auto start_column, auto end_line, auto end_column, Gfx::TextAttributes attributes, CSS::Parser::Token::Type type) {
|
||||
|
|
@ -51,18 +38,6 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
if (token.is(Parser::Token::Type::EndOfFile))
|
||||
break;
|
||||
|
||||
if (token.is(Parser::Token::Type::OpenCurly)) {
|
||||
folding_region_start_tokens.append(token);
|
||||
} else if (token.is(Parser::Token::Type::CloseCurly)) {
|
||||
if (!folding_region_start_tokens.is_empty()) {
|
||||
auto start_token = folding_region_start_tokens.take_last();
|
||||
Syntax::TextDocumentFoldingRegion folding_region;
|
||||
folding_region.range.set_start({ start_token.end_position().line, start_token.end_position().column });
|
||||
folding_region.range.set_end({ token.start_position().line, token.start_position().column });
|
||||
folding_regions.append(move(folding_region));
|
||||
}
|
||||
}
|
||||
|
||||
switch (token.type()) {
|
||||
case Parser::Token::Type::Ident:
|
||||
highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_identifier(), {} }, token.type());
|
||||
|
|
@ -149,27 +124,6 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
}
|
||||
|
||||
m_client->do_set_spans(move(spans));
|
||||
m_client->do_set_folding_regions(move(folding_regions));
|
||||
m_has_brace_buddies = false;
|
||||
highlight_matching_token_pair();
|
||||
m_client->do_update();
|
||||
}
|
||||
|
||||
Vector<Syntax::Highlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
|
||||
{
|
||||
static NeverDestroyed<Vector<Syntax::Highlighter::MatchingTokenPair>> pairs;
|
||||
if (pairs->is_empty()) {
|
||||
pairs->append({ static_cast<u64>(CSS::Parser::Token::Type::OpenCurly), static_cast<u64>(CSS::Parser::Token::Type::CloseCurly) });
|
||||
pairs->append({ static_cast<u64>(CSS::Parser::Token::Type::OpenParen), static_cast<u64>(CSS::Parser::Token::Type::CloseParen) });
|
||||
pairs->append({ static_cast<u64>(CSS::Parser::Token::Type::OpenSquare), static_cast<u64>(CSS::Parser::Token::Type::CloseSquare) });
|
||||
pairs->append({ static_cast<u64>(CSS::Parser::Token::Type::CDO), static_cast<u64>(CSS::Parser::Token::Type::CDC) });
|
||||
}
|
||||
return *pairs;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::token_types_equal(u64 token0, u64 token1) const
|
||||
{
|
||||
return token0 == token1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,8 @@ public:
|
|||
SyntaxHighlighter() = default;
|
||||
virtual ~SyntaxHighlighter() override = default;
|
||||
|
||||
virtual bool is_identifier(u64) const override;
|
||||
virtual bool is_navigatable(u64) const override;
|
||||
|
||||
virtual Syntax::Language language() const override { return Syntax::Language::CSS; }
|
||||
virtual Optional<StringView> comment_prefix() const override { return "/*"sv; }
|
||||
virtual Optional<StringView> comment_suffix() const override { return "*/"sv; }
|
||||
virtual void rehighlight(Palette const&) override;
|
||||
|
||||
protected:
|
||||
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
|
||||
virtual bool token_types_equal(u64, u64) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/NeverDestroyed.h>
|
||||
#include <LibJS/SyntaxHighlighter.h>
|
||||
#include <LibJS/Token.h>
|
||||
#include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
|
||||
|
|
@ -15,24 +14,11 @@
|
|||
|
||||
namespace Web::HTML {
|
||||
|
||||
bool SyntaxHighlighter::is_identifier(u64) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::is_navigatable(u64) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::rehighlight(Palette const& palette)
|
||||
{
|
||||
dbgln_if(SYNTAX_HIGHLIGHTING_DEBUG, "(HTML::SyntaxHighlighter) starting rehighlight");
|
||||
auto text = m_client->get_text();
|
||||
clear_nested_token_pairs();
|
||||
|
||||
// FIXME: Add folding regions for start and end tags.
|
||||
Vector<Syntax::TextDocumentFoldingRegion> folding_regions;
|
||||
Vector<Syntax::TextDocumentSpan> spans;
|
||||
auto highlight = [&](auto start_line, auto start_column, auto end_line, auto end_column, Gfx::TextAttributes attributes, AugmentedTokenKind kind) {
|
||||
if (start_line > end_line || (start_line == end_line && start_column >= end_column)) {
|
||||
|
|
@ -80,9 +66,8 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
} else if (token->is_end_tag()) {
|
||||
if (token->tag_name().is_one_of("script"sv, "style"sv)) {
|
||||
if (state == State::Javascript) {
|
||||
VERIFY(static_cast<u64>(AugmentedTokenKind::__Count) + first_free_token_kind_serial_value() < JS_TOKEN_START_VALUE);
|
||||
VERIFY(static_cast<u64>(AugmentedTokenKind::__Count) < JS_TOKEN_START_VALUE);
|
||||
Syntax::ProxyHighlighterClient proxy_client {
|
||||
*m_client,
|
||||
substring_start_position,
|
||||
JS_TOKEN_START_VALUE,
|
||||
substring_builder.string_view()
|
||||
|
|
@ -92,16 +77,13 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
highlighter.attach(proxy_client);
|
||||
highlighter.rehighlight(palette);
|
||||
highlighter.detach();
|
||||
register_nested_token_pairs(proxy_client.corrected_token_pairs(highlighter.matching_token_pairs()));
|
||||
}
|
||||
|
||||
spans.extend(proxy_client.corrected_spans());
|
||||
folding_regions.extend(proxy_client.corrected_folding_regions());
|
||||
substring_builder.clear();
|
||||
} else if (state == State::CSS) {
|
||||
VERIFY(static_cast<u64>(AugmentedTokenKind::__Count) + first_free_token_kind_serial_value() + static_cast<u64>(JS::TokenType::_COUNT_OF_TOKENS) < CSS_TOKEN_START_VALUE);
|
||||
VERIFY(static_cast<u64>(AugmentedTokenKind::__Count) + static_cast<u64>(JS::TokenType::_COUNT_OF_TOKENS) < CSS_TOKEN_START_VALUE);
|
||||
Syntax::ProxyHighlighterClient proxy_client {
|
||||
*m_client,
|
||||
substring_start_position,
|
||||
CSS_TOKEN_START_VALUE,
|
||||
substring_builder.string_view()
|
||||
|
|
@ -111,11 +93,9 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
highlighter.attach(proxy_client);
|
||||
highlighter.rehighlight(palette);
|
||||
highlighter.detach();
|
||||
register_nested_token_pairs(proxy_client.corrected_token_pairs(highlighter.matching_token_pairs()));
|
||||
}
|
||||
|
||||
spans.extend(proxy_client.corrected_spans());
|
||||
folding_regions.extend(proxy_client.corrected_folding_regions());
|
||||
substring_builder.clear();
|
||||
}
|
||||
state = State::HTML;
|
||||
|
|
@ -134,11 +114,6 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
token->end_position().column,
|
||||
{ palette.syntax_comment(), {} },
|
||||
AugmentedTokenKind::Comment);
|
||||
|
||||
Syntax::TextDocumentFoldingRegion region;
|
||||
region.range.set_start({ token->start_position().line, token->start_position().column + comment_prefix()->length() });
|
||||
region.range.set_end({ token->end_position().line, token->end_position().column - comment_suffix()->length() });
|
||||
folding_regions.append(move(region));
|
||||
} else if (token->is_start_tag() || token->is_end_tag()) {
|
||||
highlight(
|
||||
token->start_position().line,
|
||||
|
|
@ -184,24 +159,6 @@ void SyntaxHighlighter::rehighlight(Palette const& palette)
|
|||
}
|
||||
|
||||
m_client->do_set_spans(move(spans));
|
||||
m_client->do_set_folding_regions(move(folding_regions));
|
||||
m_has_brace_buddies = false;
|
||||
highlight_matching_token_pair();
|
||||
m_client->do_update();
|
||||
}
|
||||
|
||||
Vector<Syntax::Highlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
|
||||
{
|
||||
static NeverDestroyed<Vector<MatchingTokenPair>> pairs;
|
||||
if (pairs->is_empty()) {
|
||||
pairs->append({ static_cast<u64>(AugmentedTokenKind::OpenTag), static_cast<u64>(AugmentedTokenKind::CloseTag) });
|
||||
}
|
||||
return *pairs;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::token_types_equal(u64 token0, u64 token1) const
|
||||
{
|
||||
return token0 == token1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,20 +26,11 @@ public:
|
|||
SyntaxHighlighter() = default;
|
||||
virtual ~SyntaxHighlighter() override = default;
|
||||
|
||||
virtual bool is_identifier(u64) const override;
|
||||
virtual bool is_navigatable(u64) const override;
|
||||
|
||||
virtual Syntax::Language language() const override { return Syntax::Language::HTML; }
|
||||
virtual Optional<StringView> comment_prefix() const override { return "<!--"sv; }
|
||||
virtual Optional<StringView> comment_suffix() const override { return "-->"sv; }
|
||||
virtual void rehighlight(Palette const&) override;
|
||||
|
||||
static constexpr u64 JS_TOKEN_START_VALUE = 1000;
|
||||
static constexpr u64 CSS_TOKEN_START_VALUE = 2000;
|
||||
|
||||
protected:
|
||||
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
|
||||
virtual bool token_types_equal(u64, u64) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,60 +97,20 @@ SourceHighlighterClient::SourceHighlighterClient(String const& source, Syntax::L
|
|||
}
|
||||
}
|
||||
|
||||
Vector<Syntax::TextDocumentSpan> const& SourceHighlighterClient::spans() const
|
||||
{
|
||||
return document().spans();
|
||||
}
|
||||
|
||||
void SourceHighlighterClient::set_span_at_index(size_t index, Syntax::TextDocumentSpan span)
|
||||
{
|
||||
document().set_span_at_index(index, span);
|
||||
}
|
||||
|
||||
Vector<Syntax::TextDocumentFoldingRegion>& SourceHighlighterClient::folding_regions()
|
||||
{
|
||||
return document().folding_regions();
|
||||
}
|
||||
|
||||
Vector<Syntax::TextDocumentFoldingRegion> const& SourceHighlighterClient::folding_regions() const
|
||||
{
|
||||
return document().folding_regions();
|
||||
}
|
||||
|
||||
ByteString SourceHighlighterClient::highlighter_did_request_text() const
|
||||
StringView SourceHighlighterClient::highlighter_did_request_text() const
|
||||
{
|
||||
return document().text();
|
||||
}
|
||||
|
||||
void SourceHighlighterClient::highlighter_did_request_update()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
Syntax::Document& SourceHighlighterClient::highlighter_did_request_document()
|
||||
{
|
||||
return document();
|
||||
}
|
||||
|
||||
Syntax::TextPosition SourceHighlighterClient::highlighter_did_request_cursor() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void SourceHighlighterClient::highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan> spans)
|
||||
{
|
||||
document().set_spans(span_collection_index, move(spans));
|
||||
document().set_spans(move(spans));
|
||||
}
|
||||
|
||||
void SourceHighlighterClient::highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion> folding_regions)
|
||||
{
|
||||
document().set_folding_regions(move(folding_regions));
|
||||
}
|
||||
|
||||
String highlight_source(Optional<URL::URL> const& url, URL::URL const& base_url, String const& source, Syntax::Language language, HighlightOutputMode mode)
|
||||
String highlight_source(Optional<URL::URL> const& url, URL::URL const& base_url, String const& source, Syntax::Language language)
|
||||
{
|
||||
SourceHighlighterClient highlighter_client { source, language };
|
||||
return highlighter_client.to_html_string(url, base_url, mode);
|
||||
return highlighter_client.to_html_string(url, base_url);
|
||||
}
|
||||
|
||||
StringView SourceHighlighterClient::class_for_token(u64 token_type) const
|
||||
|
|
@ -268,7 +228,7 @@ StringView SourceHighlighterClient::class_for_token(u64 token_type) const
|
|||
}
|
||||
}
|
||||
|
||||
String SourceHighlighterClient::to_html_string(Optional<URL::URL> const& url, URL::URL const& base_url, HighlightOutputMode mode) const
|
||||
String SourceHighlighterClient::to_html_string(Optional<URL::URL> const& url, URL::URL const& base_url) const
|
||||
{
|
||||
StringBuilder builder;
|
||||
|
||||
|
|
@ -295,24 +255,22 @@ String SourceHighlighterClient::to_html_string(Optional<URL::URL> const& url, UR
|
|||
builder.append("</span>"sv);
|
||||
};
|
||||
|
||||
if (mode == HighlightOutputMode::FullDocument) {
|
||||
builder.append(R"~~~(
|
||||
builder.append(R"~~~(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="dark light">)~~~"sv);
|
||||
|
||||
if (url.has_value())
|
||||
builder.appendff("<title>View Source - {}</title>", escape_html_entities(url->serialize_for_display()));
|
||||
else
|
||||
builder.append("<title>View Source</title>"sv);
|
||||
if (url.has_value())
|
||||
builder.appendff("<title>View Source - {}</title>", escape_html_entities(url->serialize_for_display()));
|
||||
else
|
||||
builder.append("<title>View Source</title>"sv);
|
||||
|
||||
builder.appendff("<style type=\"text/css\">{}</style>", HTML_HIGHLIGHTER_STYLE);
|
||||
builder.append(R"~~~(
|
||||
builder.appendff("<style type=\"text/css\">{}</style>", HTML_HIGHLIGHTER_STYLE);
|
||||
builder.append(R"~~~(
|
||||
</head>
|
||||
<body>)~~~"sv);
|
||||
}
|
||||
builder.append("<pre class=\"html\">"sv);
|
||||
<body>
|
||||
<pre class=\"html\">)~~~"sv);
|
||||
|
||||
static constexpr auto href = to_array<u32>({ 'h', 'r', 'e', 'f' });
|
||||
static constexpr auto src = to_array<u32>({ 's', 'r', 'c' });
|
||||
|
|
@ -409,13 +367,11 @@ String SourceHighlighterClient::to_html_string(Optional<URL::URL> const& url, UR
|
|||
builder.append("</div>"sv);
|
||||
}
|
||||
|
||||
builder.append("</pre>"sv);
|
||||
if (mode == HighlightOutputMode::FullDocument) {
|
||||
builder.append(R"~~~(
|
||||
builder.append(R"~~~(
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
)~~~"sv);
|
||||
}
|
||||
|
||||
return builder.to_string_without_validation();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@
|
|||
|
||||
namespace WebView {
|
||||
|
||||
enum class HighlightOutputMode {
|
||||
FullDocument, // Include HTML header, title, style sheet, etc
|
||||
SourceOnly, // Just the highlighted source
|
||||
};
|
||||
|
||||
class WEBVIEW_API SourceDocument final : public Syntax::Document {
|
||||
public:
|
||||
static NonnullRefPtr<SourceDocument> create(String const& source)
|
||||
|
|
@ -53,20 +48,12 @@ public:
|
|||
SourceHighlighterClient(String const& source, Syntax::Language);
|
||||
virtual ~SourceHighlighterClient() = default;
|
||||
|
||||
String to_html_string(Optional<URL::URL> const&, URL::URL const& base_url, HighlightOutputMode) const;
|
||||
String to_html_string(Optional<URL::URL> const&, URL::URL const& base_url) const;
|
||||
|
||||
private:
|
||||
// ^ Syntax::HighlighterClient
|
||||
virtual Vector<Syntax::TextDocumentSpan> const& spans() const override;
|
||||
virtual void set_span_at_index(size_t index, Syntax::TextDocumentSpan span) override;
|
||||
virtual Vector<Syntax::TextDocumentFoldingRegion>& folding_regions() override;
|
||||
virtual Vector<Syntax::TextDocumentFoldingRegion> const& folding_regions() const override;
|
||||
virtual ByteString highlighter_did_request_text() const override;
|
||||
virtual void highlighter_did_request_update() override;
|
||||
virtual Syntax::Document& highlighter_did_request_document() override;
|
||||
virtual Syntax::TextPosition highlighter_did_request_cursor() const override;
|
||||
virtual StringView highlighter_did_request_text() const override;
|
||||
virtual void highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan>) override;
|
||||
virtual void highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion>) override;
|
||||
|
||||
StringView class_for_token(u64 token_type) const;
|
||||
|
||||
|
|
@ -76,7 +63,7 @@ private:
|
|||
OwnPtr<Syntax::Highlighter> m_highlighter;
|
||||
};
|
||||
|
||||
WEBVIEW_API String highlight_source(Optional<URL::URL> const&, URL::URL const& base_url, String const& source, Syntax::Language, HighlightOutputMode);
|
||||
WEBVIEW_API String highlight_source(Optional<URL::URL> const&, URL::URL const& base_url, String const& source, Syntax::Language);
|
||||
|
||||
constexpr inline StringView HTML_HIGHLIGHTER_STYLE = R"~~~(
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
|
|||
|
|
@ -624,7 +624,7 @@ void WebContentClient::did_request_media_context_menu(u64 page_id, Gfx::IntPoint
|
|||
void WebContentClient::did_get_source(u64, URL::URL url, URL::URL base_url, String source)
|
||||
{
|
||||
if (auto view = Application::the().open_blank_new_tab(Web::HTML::ActivateTab::Yes); view.has_value()) {
|
||||
auto html = highlight_source(url, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::FullDocument);
|
||||
auto html = highlight_source(url, base_url, source, Syntax::Language::HTML);
|
||||
view->load_html(html);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@ TEST_CASE(highlight_script_with_braces)
|
|||
// Regression test for https://github.com/LadybirdBrowser/ladybird/issues/8529
|
||||
auto source = "<script>\nfunction foo() {\n return 1;\n}\n</script>"_string;
|
||||
URL::URL base_url {};
|
||||
auto result = WebView::highlight_source({}, base_url, source, Syntax::Language::HTML, WebView::HighlightOutputMode::SourceOnly);
|
||||
auto result = WebView::highlight_source({}, base_url, source, Syntax::Language::HTML);
|
||||
EXPECT(!result.is_empty());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue