Serenity Operating System
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}