Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "Locator.h"
9#include "HackStudio.h"
10#include "Project.h"
11#include "ProjectDeclarations.h"
12#include <LibGUI/AutocompleteProvider.h>
13#include <LibGUI/BoxLayout.h>
14#include <LibGUI/FileIconProvider.h>
15#include <LibGUI/TableView.h>
16#include <LibGUI/TextBox.h>
17#include <LibGUI/Window.h>
18
19namespace HackStudio {
20
21class LocatorSuggestionModel final : public GUI::Model {
22public:
23 struct Suggestion {
24 static Suggestion create_filename(DeprecatedString const& filename);
25 static Suggestion create_symbol_declaration(CodeComprehension::Declaration const&);
26
27 bool is_filename() const { return as_filename.has_value(); }
28 bool is_symbol_declaration() const { return as_symbol_declaration.has_value(); }
29
30 Optional<DeprecatedString> as_filename;
31 Optional<CodeComprehension::Declaration> as_symbol_declaration;
32 };
33
34 explicit LocatorSuggestionModel(Vector<Suggestion>&& suggestions)
35 : m_suggestions(move(suggestions))
36 {
37 }
38
39 enum Column {
40 Icon,
41 Name,
42 Filename,
43 __Column_Count,
44 };
45 virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
46 virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
47 virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
48 {
49 auto& suggestion = m_suggestions.at(index.row());
50 if (role != GUI::ModelRole::Display)
51 return {};
52
53 if (suggestion.is_filename()) {
54 if (index.column() == Column::Name)
55 return suggestion.as_filename.value();
56 if (index.column() == Column::Filename)
57 return "";
58 if (index.column() == Column::Icon)
59 return GUI::FileIconProvider::icon_for_path(suggestion.as_filename.value());
60 }
61 if (suggestion.is_symbol_declaration()) {
62 if (index.column() == Column::Name) {
63 if (suggestion.as_symbol_declaration.value().scope.is_null())
64 return suggestion.as_symbol_declaration.value().name;
65 return DeprecatedString::formatted("{}::{}", suggestion.as_symbol_declaration.value().scope, suggestion.as_symbol_declaration.value().name);
66 }
67 if (index.column() == Column::Filename)
68 return suggestion.as_symbol_declaration.value().position.file;
69 if (index.column() == Column::Icon) {
70 auto icon = ProjectDeclarations::get_icon_for(suggestion.as_symbol_declaration.value().type);
71 if (icon.has_value())
72 return icon.value();
73 return {};
74 }
75 }
76 return {};
77 }
78
79 Vector<Suggestion> const& suggestions() const { return m_suggestions; }
80
81private:
82 Vector<Suggestion> m_suggestions;
83};
84
85LocatorSuggestionModel::Suggestion LocatorSuggestionModel::Suggestion::create_filename(DeprecatedString const& filename)
86{
87 LocatorSuggestionModel::Suggestion s;
88 s.as_filename = filename;
89 return s;
90}
91LocatorSuggestionModel::Suggestion LocatorSuggestionModel::Suggestion::create_symbol_declaration(CodeComprehension::Declaration const& decl)
92{
93 LocatorSuggestionModel::Suggestion s;
94 s.as_symbol_declaration = decl;
95 return s;
96}
97
98Locator::Locator(Core::Object* parent)
99{
100 set_layout<GUI::VerticalBoxLayout>();
101 set_fixed_height(22);
102 m_textbox = add<GUI::TextBox>();
103 m_textbox->on_change = [this] {
104 update_suggestions();
105 };
106
107 m_textbox->on_escape_pressed = [this] {
108 m_popup_window->hide();
109 m_textbox->set_focus(false);
110 };
111
112 m_textbox->on_up_pressed = [this] {
113 GUI::ModelIndex new_index = m_suggestion_view->selection().first();
114 if (new_index.is_valid())
115 new_index = m_suggestion_view->model()->index(new_index.row() - 1);
116 else
117 new_index = m_suggestion_view->model()->index(0);
118
119 if (m_suggestion_view->model()->is_within_range(new_index)) {
120 m_suggestion_view->selection().set(new_index);
121 m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
122 }
123 };
124 m_textbox->on_down_pressed = [this] {
125 GUI::ModelIndex new_index = m_suggestion_view->selection().first();
126 if (new_index.is_valid())
127 new_index = m_suggestion_view->model()->index(new_index.row() + 1);
128 else
129 new_index = m_suggestion_view->model()->index(0);
130
131 if (m_suggestion_view->model()->is_within_range(new_index)) {
132 m_suggestion_view->selection().set(new_index);
133 m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
134 }
135 };
136
137 m_textbox->on_return_pressed = [this] {
138 auto selected_index = m_suggestion_view->selection().first();
139 if (!selected_index.is_valid())
140 return;
141 open_suggestion(selected_index);
142 };
143
144 m_textbox->on_focusout = [&]() {
145 close();
146 };
147
148 m_popup_window = GUI::Window::construct(parent);
149 m_popup_window->set_window_type(GUI::WindowType::Popup);
150 m_popup_window->set_rect(0, 0, 500, 200);
151
152 m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>().release_value_but_fixme_should_propagate_errors();
153 m_suggestion_view->set_column_headers_visible(false);
154
155 m_suggestion_view->on_activation = [this](auto& index) {
156 open_suggestion(index);
157 };
158}
159
160void Locator::open_suggestion(const GUI::ModelIndex& index)
161{
162 auto& model = reinterpret_cast<LocatorSuggestionModel&>(*m_suggestion_view->model());
163 auto suggestion = model.suggestions()[index.row()];
164 if (suggestion.is_filename()) {
165 auto filename = suggestion.as_filename.value();
166 open_file(filename);
167 }
168 if (suggestion.is_symbol_declaration()) {
169 auto position = suggestion.as_symbol_declaration.value().position;
170 open_file(position.file, position.line, position.column);
171 }
172 close();
173}
174
175void Locator::open()
176{
177 m_textbox->set_focus(true);
178 if (!m_textbox->text().is_empty()) {
179 m_textbox->select_all();
180 m_popup_window->show();
181 }
182}
183
184void Locator::close()
185{
186 m_popup_window->hide();
187}
188
189void Locator::update_suggestions()
190{
191 auto typed_text = m_textbox->text();
192 Vector<LocatorSuggestionModel::Suggestion> suggestions;
193 project().for_each_text_file([&](auto& file) {
194 if (file.name().contains(typed_text, CaseSensitivity::CaseInsensitive))
195 suggestions.append(LocatorSuggestionModel::Suggestion::create_filename(file.name()));
196 });
197
198 ProjectDeclarations::the().for_each_declared_symbol([&suggestions, &typed_text](auto& decl) {
199 if (decl.name.contains(typed_text, CaseSensitivity::CaseInsensitive) || decl.scope.contains(typed_text, CaseSensitivity::CaseInsensitive))
200 suggestions.append((LocatorSuggestionModel::Suggestion::create_symbol_declaration(decl)));
201 });
202
203 dbgln("I have {} suggestion(s):", suggestions.size());
204 // Limit the debug logging otherwise this can be very slow for large projects
205 if (suggestions.size() < 100) {
206 for (auto& s : suggestions) {
207 if (s.is_filename())
208 dbgln(" {}", s.as_filename.value());
209 if (s.is_symbol_declaration())
210 dbgln(" {} ({})", s.as_symbol_declaration.value().name, s.as_symbol_declaration.value().position.file);
211 }
212 }
213
214 bool has_suggestions = !suggestions.is_empty();
215
216 m_suggestion_view->set_model(adopt_ref(*new LocatorSuggestionModel(move(suggestions))));
217
218 if (!has_suggestions)
219 m_suggestion_view->selection().clear();
220 else
221 m_suggestion_view->selection().set(m_suggestion_view->model()->index(0));
222
223 m_popup_window->move_to(screen_relative_rect().top_left().translated(0, -m_popup_window->height()));
224 dbgln("Popup rect: {}", m_popup_window->rect());
225 m_popup_window->show();
226}
227}