Serenity Operating System
at master 227 lines 8.2 kB view raw
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}