diff options
author | Andreas Kling <awesomekling@gmail.com> | 2019-10-23 20:54:41 +0200 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2019-10-23 20:54:41 +0200 |
commit | d3e81d2ba833998c78fa776cff10069a5a1832a9 (patch) | |
tree | 1861c2d8868ccb17a0426efea8583e98b35e819c /DevTools | |
parent | 41289e652f5f2de6bddfc4b8a19c159599767064 (diff) | |
download | serenity-d3e81d2ba833998c78fa776cff10069a5a1832a9.zip |
HackStudio: Start adding a "find in files" function
Projects now contain a set of TextDocument objects. Each TextDocument
represents a member file in the project. TextDocuments may not have
their file contents loaded at all times, but they will be loaded on
demand when calling TextDocument::contents().
"Find in files" works by iterating over the documents in the project
and calling find(needle) on each one. The return value from find() is
a vector of line numbers where the needle was found.
This is obviously going to need a bunch more work. :^)
Diffstat (limited to 'DevTools')
-rw-r--r-- | DevTools/HackStudio/Makefile | 1 | ||||
-rw-r--r-- | DevTools/HackStudio/Project.cpp | 10 | ||||
-rw-r--r-- | DevTools/HackStudio/Project.h | 12 | ||||
-rw-r--r-- | DevTools/HackStudio/TextDocument.cpp | 41 | ||||
-rw-r--r-- | DevTools/HackStudio/TextDocument.h | 29 | ||||
-rw-r--r-- | DevTools/HackStudio/main.cpp | 156 |
6 files changed, 210 insertions, 39 deletions
diff --git a/DevTools/HackStudio/Makefile b/DevTools/HackStudio/Makefile index 23bcd506eb..657c96beb4 100644 --- a/DevTools/HackStudio/Makefile +++ b/DevTools/HackStudio/Makefile @@ -2,6 +2,7 @@ include ../../Makefile.common OBJS = \ Project.o \ + TextDocument.o \ TerminalWrapper.o \ main.o diff --git a/DevTools/HackStudio/Project.cpp b/DevTools/HackStudio/Project.cpp index 30acbc7729..9ef4f7e44c 100644 --- a/DevTools/HackStudio/Project.cpp +++ b/DevTools/HackStudio/Project.cpp @@ -14,11 +14,11 @@ public: { int row = index.row(); if (role == Role::Display) { - return m_project.m_files.at(row); + return m_project.m_files.at(row).name(); } if (role == Role::Font) { extern String g_currently_open_file; - if (m_project.m_files.at(row) == g_currently_open_file) + if (m_project.m_files.at(row).name() == g_currently_open_file) return Font::default_bold_font(); return {}; } @@ -30,9 +30,11 @@ private: Project& m_project; }; -Project::Project(Vector<String>&& files) - : m_files(move(files)) +Project::Project(Vector<String>&& filenames) { + for (auto& filename : filenames) { + m_files.append(TextDocument::construct_with_name(filename)); + } m_model = adopt(*new ProjectModel(*this)); } diff --git a/DevTools/HackStudio/Project.h b/DevTools/HackStudio/Project.h index 3acc043e18..8233ac6cb2 100644 --- a/DevTools/HackStudio/Project.h +++ b/DevTools/HackStudio/Project.h @@ -1,6 +1,8 @@ #pragma once +#include "TextDocument.h" #include <AK/Noncopyable.h> +#include <AK/NonnullRefPtrVector.h> #include <AK/OwnPtr.h> #include <LibGUI/GModel.h> @@ -12,10 +14,18 @@ public: GModel& model() { return *m_model; } + template<typename Callback> + void for_each_text_file(Callback callback) const + { + for (auto& file : m_files) { + callback(file); + } + } + private: friend class ProjectModel; explicit Project(Vector<String>&& files); RefPtr<GModel> m_model; - Vector<String> m_files; + NonnullRefPtrVector<TextDocument> m_files; }; diff --git a/DevTools/HackStudio/TextDocument.cpp b/DevTools/HackStudio/TextDocument.cpp new file mode 100644 index 0000000000..6dd74b3c04 --- /dev/null +++ b/DevTools/HackStudio/TextDocument.cpp @@ -0,0 +1,41 @@ +#include "TextDocument.h" +#include <LibCore/CFile.h> +#include <string.h> + +const ByteBuffer& TextDocument::contents() const +{ + if (m_contents.is_null()) { + auto file = CFile::construct(m_name); + if (file->open(CFile::ReadOnly)) + m_contents = file->read_all(); + } + return m_contents; +} + +Vector<int> TextDocument::find(const StringView& needle) const +{ + // NOTE: This forces us to load the contents if we hadn't already. + contents(); + + Vector<int> matching_line_numbers; + + String needle_as_string(needle); + + int line_index = 0; + int start_of_line = 0; + for (int i = 0; i < m_contents.size(); ++i) { + char ch = m_contents[i]; + if (ch == '\n') { + // FIXME: Please come back here and do this the good boy way. + String line(StringView(m_contents.data() + start_of_line, i - start_of_line)); + auto* found = strstr(line.characters(), needle_as_string.characters()); + if (found) + matching_line_numbers.append(line_index + 1); + ++line_index; + start_of_line = i + 1; + continue; + } + } + + return matching_line_numbers; +} diff --git a/DevTools/HackStudio/TextDocument.h b/DevTools/HackStudio/TextDocument.h new file mode 100644 index 0000000000..81cd1a51e5 --- /dev/null +++ b/DevTools/HackStudio/TextDocument.h @@ -0,0 +1,29 @@ +#pragma once + +#include <AK/ByteBuffer.h> +#include <AK/NonnullRefPtr.h> +#include <AK/RefCounted.h> +#include <AK/String.h> + +class TextDocument : public RefCounted<TextDocument> { +public: + static NonnullRefPtr<TextDocument> construct_with_name(const String& name) + { + return adopt(*new TextDocument(name)); + } + + const String& name() const { return m_name; } + + const ByteBuffer& contents() const; + + Vector<int> find(const StringView&) const; + +private: + explicit TextDocument(const String& name) + : m_name(name) + { + } + + String m_name; + mutable ByteBuffer m_contents; +}; diff --git a/DevTools/HackStudio/main.cpp b/DevTools/HackStudio/main.cpp index 1b9fa097e0..f0452d57d0 100644 --- a/DevTools/HackStudio/main.cpp +++ b/DevTools/HackStudio/main.cpp @@ -4,6 +4,7 @@ #include <LibGUI/GAboutDialog.h> #include <LibGUI/GAction.h> #include <LibGUI/GApplication.h> +#include <LibGUI/GButton.h> #include <LibGUI/GBoxLayout.h> #include <LibGUI/GInputBox.h> #include <LibGUI/GListView.h> @@ -13,6 +14,7 @@ #include <LibGUI/GSplitter.h> #include <LibGUI/GStatusBar.h> #include <LibGUI/GTabWidget.h> +#include <LibGUI/GTextBox.h> #include <LibGUI/GTextEditor.h> #include <LibGUI/GToolBar.h> #include <LibGUI/GWidget.h> @@ -21,20 +23,26 @@ #include <unistd.h> String g_currently_open_file; +OwnPtr<Project> g_project; +RefPtr<GWindow> g_window; +RefPtr<GListView> g_project_list_view; +RefPtr<GTextEditor> g_text_editor; static void build(TerminalWrapper&); static void run(TerminalWrapper&); +static NonnullRefPtr<GWidget> build_find_in_files_widget(); +static void open_file(const String&); int main(int argc, char** argv) { GApplication app(argc, argv); - auto window = GWindow::construct(); - window->set_rect(100, 100, 800, 600); - window->set_title("HackStudio"); + g_window = GWindow::construct(); + g_window->set_rect(100, 100, 800, 600); + g_window->set_title("HackStudio"); auto widget = GWidget::construct(); - window->set_main_widget(widget); + g_window->set_main_widget(widget); widget->set_fill_with_background_color(true); widget->set_layout(make<GBoxLayout>(Orientation::Vertical)); @@ -44,65 +52,59 @@ int main(int argc, char** argv) perror("chdir"); return 1; } - auto project = Project::load_from_file("little.files"); - ASSERT(project); + g_project = Project::load_from_file("little.files"); + ASSERT(g_project); auto toolbar = GToolBar::construct(widget); auto outer_splitter = GSplitter::construct(Orientation::Horizontal, widget); - auto project_list_view = GListView::construct(outer_splitter); - project_list_view->set_model(project->model()); - project_list_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); - project_list_view->set_preferred_size(200, 0); + g_project_list_view = GListView::construct(outer_splitter); + g_project_list_view->set_model(g_project->model()); + g_project_list_view->set_size_policy(SizePolicy::Fixed, SizePolicy::Fill); + g_project_list_view->set_preferred_size(200, 0); auto inner_splitter = GSplitter::construct(Orientation::Vertical, outer_splitter); - auto text_editor = GTextEditor::construct(GTextEditor::MultiLine, inner_splitter); - text_editor->set_ruler_visible(true); - - project_list_view->on_activation = [&](auto& index) { - auto filename = project_list_view->model()->data(index).to_string(); - auto file = CFile::construct(filename); - if (!file->open(CFile::ReadOnly)) { - GMessageBox::show("Could not open!", "Error", GMessageBox::Type::Error, GMessageBox::InputType::OK, window); - return; - } - text_editor->set_text(file->read_all()); - g_currently_open_file = filename; - window->set_title(String::format("%s - HackStudio", g_currently_open_file.characters())); - project_list_view->update(); + g_text_editor = GTextEditor::construct(GTextEditor::MultiLine, inner_splitter); + g_text_editor->set_ruler_visible(true); + + g_project_list_view->on_activation = [&](auto& index) { + auto filename = g_project_list_view->model()->data(index).to_string(); + open_file(filename); }; auto tab_widget = GTabWidget::construct(inner_splitter); + tab_widget->add_widget("Find in files", build_find_in_files_widget()); + auto terminal_wrapper = TerminalWrapper::construct(nullptr); tab_widget->add_widget("Console", terminal_wrapper); auto statusbar = GStatusBar::construct(widget); - text_editor->on_cursor_change = [&] { - statusbar->set_text(String::format("Line: %d, Column: %d", text_editor->cursor().line(), text_editor->cursor().column())); + g_text_editor->on_cursor_change = [&] { + statusbar->set_text(String::format("Line: %d, Column: %d", g_text_editor->cursor().line(), g_text_editor->cursor().column())); }; - text_editor->add_custom_context_menu_action(GAction::create( + g_text_editor->add_custom_context_menu_action(GAction::create( "Go to line...", { Mod_Ctrl, Key_L }, GraphicsBitmap::load_from_file("/res/icons/16x16/go-forward.png"), [&](auto&) { - auto input_box = GInputBox::construct("Line:", "Go to line", window); + auto input_box = GInputBox::construct("Line:", "Go to line", g_window); auto result = input_box->exec(); if (result == GInputBox::ExecOK) { bool ok; auto line_number = input_box->text_value().to_uint(ok); if (ok) { - text_editor->set_cursor(line_number - 1, 0); + g_text_editor->set_cursor(line_number - 1, 0); } } }, - text_editor)); + g_text_editor)); auto menubar = make<GMenuBar>(); auto app_menu = make<GMenu>("HackStudio"); app_menu->add_action(GAction::create("Save", { Mod_Ctrl, Key_S }, GraphicsBitmap::load_from_file("/res/icons/16x16/save.png"), [&](auto&) { if (g_currently_open_file.is_empty()) return; - text_editor->write_to_file(g_currently_open_file); + g_text_editor->write_to_file(g_currently_open_file); })); app_menu->add_action(GCommonActions::make_quit_action([&](auto&) { app.quit(); @@ -122,15 +124,15 @@ int main(int argc, char** argv) auto help_menu = make<GMenu>("Help"); help_menu->add_action(GAction::create("About", [&](auto&) { - GAboutDialog::show("HackStudio", small_icon, window); + GAboutDialog::show("HackStudio", small_icon, g_window); })); menubar->add_menu(move(help_menu)); app.set_menubar(move(menubar)); - window->set_icon(small_icon); + g_window->set_icon(small_icon); - window->show(); + g_window->show(); return app.exec(); } @@ -143,3 +145,89 @@ void run(TerminalWrapper& wrapper) { wrapper.run_command("make run"); } + +struct FilenameAndLineNumber { + String filename; + int line_number { -1 }; +}; + +class SearchResultsModel final : public GModel { +public: + explicit SearchResultsModel(const Vector<FilenameAndLineNumber>&& matches) + : m_matches(move(matches)) + { + } + + virtual int row_count(const GModelIndex& = GModelIndex()) const override { return m_matches.size(); } + virtual int column_count(const GModelIndex& = GModelIndex()) const override { return 1; } + virtual GVariant data(const GModelIndex& index, Role role = Role::Display) const override + { + if (role == Role::Display) { + auto& match = m_matches.at(index.row()); + return String::format("%s:%d", match.filename.characters(), match.line_number); + } + return {}; + } + virtual void update() override {} + +private: + Vector<FilenameAndLineNumber> m_matches; +}; + +static RefPtr<SearchResultsModel> find_in_files(const StringView& text) +{ + Vector<FilenameAndLineNumber> matches; + g_project->for_each_text_file([&](auto& file) { + auto matches_in_file = file.find(text); + for (int match : matches_in_file) { + matches.append({ file.name(), match }); + } + }); + + return adopt(*new SearchResultsModel(move(matches))); +} + +NonnullRefPtr<GWidget> build_find_in_files_widget() +{ + auto widget = GWidget::construct(); + widget->set_layout(make<GBoxLayout>(Orientation::Vertical)); + auto textbox = GTextBox::construct(widget); + textbox->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + textbox->set_preferred_size(0, 20); + auto button = GButton::construct("Find in files", widget); + button->set_size_policy(SizePolicy::Fill, SizePolicy::Fixed); + button->set_preferred_size(0, 20); + + auto result_view = GListView::construct(widget); + + result_view->on_activation = [result_view](auto& index) { + auto match_string = result_view->model()->data(index).to_string(); + auto parts = match_string.split(':'); + ASSERT(parts.size() == 2); + bool ok; + int line_number = parts[1].to_int(ok); + ASSERT(ok); + open_file(parts[0]); + g_text_editor->set_cursor(line_number - 1, 0); + g_text_editor->set_focus(true); + }; + + button->on_click = [textbox, result_view = result_view.ptr()](auto&) { + auto results_model = find_in_files(textbox->text()); + result_view->set_model(results_model); + }; + return widget; +} + +void open_file(const String& filename) +{ + auto file = CFile::construct(filename); + if (!file->open(CFile::ReadOnly)) { + GMessageBox::show("Could not open!", "Error", GMessageBox::Type::Error, GMessageBox::InputType::OK, g_window); + return; + } + g_text_editor->set_text(file->read_all()); + g_currently_open_file = filename; + g_window->set_title(String::format("%s - HackStudio", g_currently_open_file.characters())); + g_project_list_view->update(); +} |