Serenity Operating System
at master 208 lines 8.0 kB view raw
1/* 2 * Copyright (c) 2020-2022, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <LibGUI/AutocompleteProvider.h> 8#include <LibGUI/BoxLayout.h> 9#include <LibGUI/Model.h> 10#include <LibGUI/TableView.h> 11#include <LibGUI/TextEditor.h> 12#include <LibGUI/Window.h> 13#include <LibGfx/Bitmap.h> 14 15static RefPtr<Gfx::Bitmap> s_cpp_identifier_icon; 16static RefPtr<Gfx::Bitmap> s_unspecified_identifier_icon; 17 18namespace GUI { 19 20class AutocompleteSuggestionModel final : public GUI::Model { 21public: 22 explicit AutocompleteSuggestionModel(Vector<CodeComprehension::AutocompleteResultEntry>&& suggestions) 23 : m_suggestions(move(suggestions)) 24 { 25 } 26 27 enum Column { 28 Icon, 29 Name, 30 __Column_Count, 31 }; 32 33 enum InternalRole { 34 __ModelRoleCustom = (int)GUI::ModelRole::Custom, 35 PartialInputLength, 36 Completion, 37 HideAutocompleteAfterApplying, 38 }; 39 40 virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); } 41 virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; } 42 virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override 43 { 44 auto& suggestion = m_suggestions.at(index.row()); 45 if (role == GUI::ModelRole::Display) { 46 if (index.column() == Column::Name) { 47 if (!suggestion.display_text.is_empty()) 48 return suggestion.display_text; 49 else 50 return suggestion.completion; 51 } 52 if (index.column() == Column::Icon) { 53 if (suggestion.language == CodeComprehension::Language::Cpp) { 54 if (!s_cpp_identifier_icon) { 55 s_cpp_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/cpp-identifier.png"sv).release_value_but_fixme_should_propagate_errors(); 56 } 57 return *s_cpp_identifier_icon; 58 } 59 if (suggestion.language == CodeComprehension::Language::Unspecified) { 60 if (!s_unspecified_identifier_icon) { 61 s_unspecified_identifier_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/completion/unspecified-identifier.png"sv).release_value_but_fixme_should_propagate_errors(); 62 } 63 return *s_unspecified_identifier_icon; 64 } 65 return {}; 66 } 67 } 68 69 if ((int)role == InternalRole::PartialInputLength) 70 return (i64)suggestion.partial_input_length; 71 72 if ((int)role == InternalRole::Completion) 73 return suggestion.completion; 74 75 if ((int)role == InternalRole::HideAutocompleteAfterApplying) 76 return suggestion.hide_autocomplete_after_applying == CodeComprehension::AutocompleteResultEntry::HideAutocompleteAfterApplying::Yes; 77 78 return {}; 79 } 80 81 void set_suggestions(Vector<CodeComprehension::AutocompleteResultEntry>&& suggestions) { m_suggestions = move(suggestions); } 82 83private: 84 Vector<CodeComprehension::AutocompleteResultEntry> m_suggestions; 85}; 86 87AutocompleteBox::AutocompleteBox(TextEditor& editor) 88 : m_editor(editor) 89{ 90 m_popup_window = GUI::Window::construct(m_editor->window()); 91 m_popup_window->set_window_type(GUI::WindowType::Autocomplete); 92 m_popup_window->set_obey_widget_min_size(false); 93 m_popup_window->set_rect(0, 0, 175, 25); 94 95 auto main_widget = m_popup_window->set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors(); 96 main_widget->set_fill_with_background_color(true); 97 main_widget->set_layout<GUI::VerticalBoxLayout>(); 98 99 m_suggestion_view = main_widget->add<GUI::TableView>(); 100 m_suggestion_view->set_frame_shadow(Gfx::FrameShadow::Plain); 101 m_suggestion_view->set_frame_thickness(1); 102 m_suggestion_view->set_column_headers_visible(false); 103 m_suggestion_view->set_visible(false); 104 m_suggestion_view->on_activation = [&](GUI::ModelIndex const& index) { 105 if (!m_suggestion_view->model()->is_within_range(index)) 106 return; 107 m_suggestion_view->selection().set(index); 108 m_suggestion_view->scroll_into_view(index, Orientation::Vertical); 109 apply_suggestion(); 110 }; 111 112 m_no_suggestions_view = main_widget->add<GUI::Label>("No suggestions"); 113} 114 115void AutocompleteBox::update_suggestions(Vector<CodeComprehension::AutocompleteResultEntry>&& suggestions) 116{ 117 // FIXME: There's a potential race here if, after the user selected an autocomplete suggestion, 118 // the LanguageServer sends an update and this function is executed before AutocompleteBox::apply_suggestion() 119 // is executed. 120 121 bool has_suggestions = !suggestions.is_empty(); 122 if (m_suggestion_view->model()) { 123 auto& model = *static_cast<AutocompleteSuggestionModel*>(m_suggestion_view->model()); 124 model.set_suggestions(move(suggestions)); 125 } else { 126 m_suggestion_view->set_model(adopt_ref(*new AutocompleteSuggestionModel(move(suggestions)))); 127 } 128 129 m_suggestion_view->model()->invalidate(); 130 131 if (has_suggestions) 132 m_suggestion_view->set_cursor(m_suggestion_view->model()->index(0), GUI::AbstractView::SelectionUpdate::Set); 133 134 m_suggestion_view->set_visible(has_suggestions); 135 m_suggestion_view->set_focus(has_suggestions); 136 m_no_suggestions_view->set_visible(!has_suggestions); 137 m_popup_window->resize(has_suggestions ? Gfx::IntSize(300, 100) : Gfx::IntSize(175, 25)); 138 139 m_suggestion_view->update(); 140} 141 142bool AutocompleteBox::is_visible() const 143{ 144 return m_popup_window->is_visible(); 145} 146 147void AutocompleteBox::show(Gfx::IntPoint suggestion_box_location) 148{ 149 if (!m_suggestion_view->model()) 150 return; 151 152 m_popup_window->move_to(suggestion_box_location); 153 m_popup_window->show(); 154} 155 156void AutocompleteBox::close() 157{ 158 m_popup_window->hide(); 159} 160 161void AutocompleteBox::next_suggestion() 162{ 163 m_suggestion_view->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); 164} 165 166void AutocompleteBox::previous_suggestion() 167{ 168 m_suggestion_view->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set); 169} 170 171CodeComprehension::AutocompleteResultEntry::HideAutocompleteAfterApplying AutocompleteBox::apply_suggestion() 172{ 173 auto hide_when_done = CodeComprehension::AutocompleteResultEntry::HideAutocompleteAfterApplying::Yes; 174 175 if (m_editor.is_null()) 176 return hide_when_done; 177 178 if (!m_editor->is_editable()) 179 return hide_when_done; 180 181 auto selected_index = m_suggestion_view->selection().first(); 182 if (!selected_index.is_valid() || !m_suggestion_view->model()->is_within_range(selected_index)) 183 return hide_when_done; 184 185 auto suggestion_index = m_suggestion_view->model()->index(selected_index.row()); 186 auto completion = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::Completion).to_deprecated_string(); 187 size_t partial_length = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::PartialInputLength).to_i64(); 188 auto hide_after_applying = suggestion_index.data((GUI::ModelRole)AutocompleteSuggestionModel::InternalRole::HideAutocompleteAfterApplying).to_bool(); 189 190 if (!hide_after_applying) 191 hide_when_done = CodeComprehension::AutocompleteResultEntry::HideAutocompleteAfterApplying::No; 192 193 VERIFY(completion.length() >= partial_length); 194 if (!m_editor->has_selection()) { 195 auto cursor = m_editor->cursor(); 196 VERIFY(m_editor->cursor().column() >= partial_length); 197 198 TextPosition start(cursor.line(), cursor.column() - partial_length); 199 auto end = cursor; 200 m_editor->delete_text_range(TextRange(start, end)); 201 } 202 203 m_editor->insert_at_cursor_or_replace_selection(completion); 204 205 return hide_when_done; 206} 207 208}