1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-14 11:14:58 +00:00
serenity/Userland/Libraries/LibGUI/AutocompleteProvider.cpp
thislooksfun a5b3c3f85f LibGUI: Allow completion suggestions to fill and display different text
There are times when it is nice to display one suggestion but fill
something different. This lays the groundwork for allowing
GMLAutocompleteProvider to automatically add ': ' to the end of
suggested properties, while keeping the ': ' suffix from cluttering up
the suggestion UI.
2021-11-02 17:53:22 +01:00

220 lines
7.8 KiB
C++

/*
* Copyright (c) 2020, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGUI/AutocompleteProvider.h>
#include <LibGUI/Model.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/Window.h>
#include <LibGfx/Bitmap.h>
static RefPtr<Gfx::Bitmap> s_cpp_identifier_icon;
static RefPtr<Gfx::Bitmap> s_unspecified_identifier_icon;
namespace GUI {
class AutocompleteSuggestionModel final : public GUI::Model {
public:
explicit AutocompleteSuggestionModel(Vector<AutocompleteProvider::Entry>&& suggestions)
: m_suggestions(move(suggestions))
{
}
enum Column {
Icon,
Name,
__Column_Count,
};
enum InternalRole {
__ModelRoleCustom = (int)GUI::ModelRole::Custom,
PartialInputLength,
Kind,
Completion,
};
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
auto& suggestion = m_suggestions.at(index.row());
if (role == GUI::ModelRole::Display) {
if (index.column() == Column::Name) {
if (!suggestion.display_text.is_empty())
return suggestion.display_text;
else
return suggestion.completion;
}
if (index.column() == Column::Icon) {
if (suggestion.language == GUI::AutocompleteProvider::Language::Cpp) {
if (!s_cpp_identifier_icon) {
s_cpp_identifier_icon = Gfx::Bitmap::try_load_from_file("/res/icons/16x16/completion/cpp-identifier.png");
}
return *s_cpp_identifier_icon;
}
if (suggestion.language == GUI::AutocompleteProvider::Language::Unspecified) {
if (!s_unspecified_identifier_icon) {
s_unspecified_identifier_icon = Gfx::Bitmap::try_load_from_file("/res/icons/16x16/completion/unspecified-identifier.png");
}
return *s_unspecified_identifier_icon;
}
return {};
}
}
if ((int)role == InternalRole::Kind)
return (u32)suggestion.kind;
if ((int)role == InternalRole::PartialInputLength)
return (i64)suggestion.partial_input_length;
if ((int)role == InternalRole::Completion)
return suggestion.completion;
return {};
}
void set_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions) { m_suggestions = move(suggestions); }
private:
Vector<AutocompleteProvider::Entry> m_suggestions;
};
AutocompleteBox::~AutocompleteBox() { }
AutocompleteBox::AutocompleteBox(TextEditor& editor)
: m_editor(editor)
{
m_popup_window = GUI::Window::construct(m_editor->window());
m_popup_window->set_window_type(GUI::WindowType::Tooltip);
m_popup_window->set_rect(0, 0, 300, 100);
m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>();
m_suggestion_view->set_column_headers_visible(false);
}
void AutocompleteBox::update_suggestions(Vector<AutocompleteProvider::Entry>&& suggestions)
{
// FIXME: There's a potential race here if, after the user selected an autocomplete suggestion,
// the LanguageServer sends an update and this function is executed before AutocompleteBox::apply_suggestion()
// is executed.
bool has_suggestions = !suggestions.is_empty();
if (m_suggestion_view->model()) {
auto& model = *static_cast<AutocompleteSuggestionModel*>(m_suggestion_view->model());
model.set_suggestions(move(suggestions));
} else {
m_suggestion_view->set_model(adopt_ref(*new AutocompleteSuggestionModel(move(suggestions))));
m_suggestion_view->update();
if (has_suggestions)
m_suggestion_view->set_cursor(m_suggestion_view->model()->index(0), GUI::AbstractView::SelectionUpdate::Set);
}
m_suggestion_view->model()->invalidate();
m_suggestion_view->update();
if (!has_suggestions)
close();
}
bool AutocompleteBox::is_visible() const
{
return m_popup_window->is_visible();
}
void AutocompleteBox::show(Gfx::IntPoint suggestion_box_location)
{
if (!m_suggestion_view->model() || m_suggestion_view->model()->row_count() == 0)
return;
m_popup_window->move_to(suggestion_box_location);
if (!is_visible())
m_suggestion_view->move_cursor(GUI::AbstractView::CursorMovement::Home, GUI::AbstractTableView::SelectionUpdate::Set);
m_popup_window->show();
}
void AutocompleteBox::close()
{
m_popup_window->hide();
}
void AutocompleteBox::next_suggestion()
{
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() + 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_within_range(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
}
void AutocompleteBox::previous_suggestion()
{
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() - 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_within_range(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
}
void AutocompleteBox::apply_suggestion()
{
if (m_editor.is_null())
return;
if (!m_editor->is_editable())
return;
auto selected_index = m_suggestion_view->selection().first();
if (!selected_index.is_valid() || !m_suggestion_view->model()->is_within_range(selected_index))
return;
auto suggestion_index = m_suggestion_view->model()->index(selected_index.row());
auto suggestion = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::Completion).to_string();
size_t partial_length = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::PartialInputLength).to_i64();
VERIFY(suggestion.length() >= partial_length);
if (!m_editor->has_selection()) {
auto cursor = m_editor->cursor();
VERIFY(m_editor->cursor().column() >= partial_length);
TextPosition start(cursor.line(), cursor.column() - partial_length);
auto end = cursor;
m_editor->delete_text_range(TextRange(start, end));
}
auto completion_kind = (GUI::AutocompleteProvider::CompletionKind)suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::Kind).as_u32();
String completion;
if (suggestion.ends_with(".h") && completion_kind == GUI::AutocompleteProvider::CompletionKind::SystemInclude)
completion = String::formatted("{}{}", suggestion, ">");
else if (suggestion.ends_with(".h") && completion_kind == GUI::AutocompleteProvider::CompletionKind::ProjectInclude)
completion = String::formatted("{}{}", suggestion, "\"");
else
completion = suggestion;
m_editor->insert_at_cursor_or_replace_selection(completion);
}
bool AutocompleteProvider::Declaration::operator==(const AutocompleteProvider::Declaration& other) const
{
return name == other.name && position == other.position && type == other.type && scope == other.scope;
}
bool AutocompleteProvider::ProjectLocation::operator==(const ProjectLocation& other) const
{
return file == other.file && line == other.line && column == other.column;
}
}