GTextEditor: Implement a simple text search API

- GTextRange find(const StringView& needle, const GTextPosition& start)

This function searches for the needle in the haystack (the full text)
and returns a GTextRange for the closest match after "start".
If the needle is not found, it returns an invalid GTextRange.
If no "start" position is provided, the search begins at the head of
the text document. :^)
This commit is contained in:
Andreas Kling 2019-08-21 21:23:17 +02:00
parent 5670a3e064
commit 0c72371ad9
Notes: sideshowbarker 2024-07-19 12:34:15 +09:00
2 changed files with 116 additions and 14 deletions

View file

@ -668,21 +668,27 @@ int GTextEditor::content_x_for_position(const GTextPosition& position) const
}
}
Rect GTextEditor::cursor_content_rect() const
Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const
{
if (!m_cursor.is_valid())
if (!position.is_valid())
return {};
ASSERT(!m_lines.is_empty());
ASSERT(m_cursor.column() <= (current_line().length() + 1));
ASSERT(position.column() <= (current_line().length() + 1));
int cursor_x = content_x_for_position(m_cursor);
int x = content_x_for_position(position);
if (is_single_line()) {
Rect cursor_rect { cursor_x, 0, 1, font().glyph_height() + 2 };
cursor_rect.center_vertically_within({ {}, frame_inner_rect().size() });
return cursor_rect;
Rect rect { x, 0, 1, font().glyph_height() + 2 };
rect.center_vertically_within({ {}, frame_inner_rect().size() });
return rect;
}
return { cursor_x, m_cursor.line() * line_height(), 1, line_height() };
return { x, position.line() * line_height(), 1, line_height() };
}
Rect GTextEditor::cursor_content_rect() const
{
return content_rect_for_position(m_cursor);
}
Rect GTextEditor::line_widget_rect(int line_index) const
@ -696,14 +702,19 @@ Rect GTextEditor::line_widget_rect(int line_index) const
return rect;
}
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);
scroll_into_view(rect, true, true);
}
void GTextEditor::scroll_cursor_into_view()
{
auto rect = cursor_content_rect();
if (m_cursor.column() == 0)
rect.set_x(content_x_for_position({ m_cursor.line(), 0 }) - 2);
else if (m_cursor.column() == m_lines[m_cursor.line()].length())
rect.set_x(content_x_for_position({ m_cursor.line(), m_lines[m_cursor.line()].length() }) + 2);
scroll_into_view(rect, true, true);
scroll_position_into_view(m_cursor);
}
Rect GTextEditor::line_content_rect(int line_index) const
@ -1079,3 +1090,64 @@ void GTextEditor::resize_event(GResizeEvent& event)
GScrollableWidget::resize_event(event);
update_content_size();
}
GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap)
{
auto& line = m_lines[position.line()];
if (position.column() == line.length()) {
if (position.line() == line_count() - 1) {
if (should_wrap == ShouldWrapAtEndOfDocument::Yes)
return { 0, 0 };
return {};
}
return { position.line() + 1, 0 };
}
return { position.line(), position.column() + 1 };
}
GTextRange GTextEditor::find(const StringView& needle, const GTextPosition& start)
{
if (needle.is_empty())
return {};
GTextPosition position = start.is_valid() ? start : GTextPosition(0, 0);
GTextPosition original_position = position;
GTextPosition start_of_potential_match;
int needle_index = 0;
do {
auto ch = character_at(position);
if (ch == needle[needle_index]) {
if (needle_index == 0)
start_of_potential_match = position;
++needle_index;
if (needle_index >= needle.length())
return { start_of_potential_match, next_position_after(position) };
} else {
needle_index = 0;
}
position = next_position_after(position);
} while(position.is_valid() && position != original_position);
return {};
}
void GTextEditor::set_selection(const GTextRange& selection)
{
if (m_selection == selection)
return;
m_selection = selection;
set_cursor(m_selection.end());
scroll_position_into_view(normalized_selection().start());
update();
}
char GTextEditor::character_at(const GTextPosition& position) const
{
ASSERT(position.line() < line_count());
auto& line = m_lines[position.line()];
if (position.column() == line.length())
return '\n';
return line.characters()[position.column()];
}

View file

@ -11,6 +11,8 @@ class GMenu;
class GScrollBar;
class Painter;
enum class ShouldWrapAtEndOfDocument { No = 0, Yes };
class GTextPosition {
public:
GTextPosition() {}
@ -69,6 +71,11 @@ public:
m_end = end;
}
bool operator==(const GTextRange& other) const
{
return m_start == other.m_start && m_end == other.m_end;
}
private:
GTextPosition normalized_start() const { return m_start < m_end ? m_start : m_end; }
GTextPosition normalized_end() const { return m_start < m_end ? m_end : m_start; }
@ -108,6 +115,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_spacing() const { return m_line_spacing; }
int line_height() const { return font().glyph_height() + m_line_spacing; }
@ -118,8 +126,13 @@ public:
bool write_to_file(const StringView& path);
GTextRange find(const StringView&, const GTextPosition& start = {});
GTextPosition next_position_after(const GTextPosition&, ShouldWrapAtEndOfDocument = ShouldWrapAtEndOfDocument::Yes);
bool has_selection() const { return m_selection.is_valid(); }
String selected_text() const;
void set_selection(const GTextRange&);
String text() const;
void clear();
@ -192,6 +205,7 @@ private:
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();
void set_cursor(int line, int column);
void set_cursor(const GTextPosition&);
@ -207,6 +221,7 @@ private:
void delete_selection();
void did_update_selection();
int content_x_for_position(const GTextPosition&) const;
char character_at(const GTextPosition&) const;
Type m_type { MultiLine };
@ -232,3 +247,18 @@ private:
RefPtr<GAction> m_delete_action;
CElapsedTimer m_triple_click_timer;
};
inline const LogStream& operator<<(const LogStream& stream, const GTextPosition& value)
{
if (!value.is_valid())
return stream << "GTextPosition(Invalid)";
return stream << String::format("(%d,%d)", value.line(), value.column());
}
inline const LogStream& operator<<(const LogStream& stream, const GTextRange& value)
{
if (!value.is_valid())
return stream << "GTextRange(Invalid)";
return stream << value.start() << '-' << value.end();
}