Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "FindInFilesWidget.h"
8#include "HackStudio.h"
9#include "Project.h"
10#include <AK/StringBuilder.h>
11#include <LibGUI/BoxLayout.h>
12#include <LibGUI/Button.h>
13#include <LibGUI/TableView.h>
14#include <LibGUI/TextBox.h>
15#include <LibGfx/Font/FontDatabase.h>
16
17namespace HackStudio {
18
19struct Match {
20 DeprecatedString filename;
21 GUI::TextRange range;
22 DeprecatedString text;
23};
24
25class SearchResultsModel final : public GUI::Model {
26public:
27 enum Column {
28 Filename,
29 Location,
30 MatchedText,
31 __Count
32 };
33
34 explicit SearchResultsModel(Vector<Match> const&& matches)
35 : m_matches(move(matches))
36 {
37 }
38
39 virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_matches.size(); }
40 virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
41
42 virtual DeprecatedString column_name(int column) const override
43 {
44 switch (column) {
45 case Column::Filename:
46 return "Filename";
47 case Column::Location:
48 return "#";
49 case Column::MatchedText:
50 return "Text";
51 default:
52 VERIFY_NOT_REACHED();
53 }
54 }
55
56 virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
57 {
58 if (role == GUI::ModelRole::TextAlignment)
59 return Gfx::TextAlignment::CenterLeft;
60 if (role == GUI::ModelRole::Font) {
61 if (index.column() == Column::MatchedText)
62 return Gfx::FontDatabase::default_fixed_width_font();
63 return {};
64 }
65 if (role == GUI::ModelRole::Display) {
66 auto& match = m_matches.at(index.row());
67 switch (index.column()) {
68 case Column::Filename:
69 return match.filename;
70 case Column::Location:
71 return (int)match.range.start().line();
72 case Column::MatchedText:
73 return match.text;
74 }
75 }
76 return {};
77 }
78
79 virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override
80 {
81 if (row < 0 || row >= (int)m_matches.size())
82 return {};
83 if (column < 0 || column >= Column::__Count)
84 return {};
85 return create_index(row, column, &m_matches.at(row));
86 }
87
88private:
89 Vector<Match> m_matches;
90};
91
92static RefPtr<SearchResultsModel> find_in_files(StringView text)
93{
94 Vector<Match> matches;
95 project().for_each_text_file([&](auto& file) {
96 auto matches_in_file = file.document().find_all(text);
97 for (auto& range : matches_in_file) {
98 auto whole_line_range = file.document().range_for_entire_line(range.start().line());
99 auto whole_line_containing_match = file.document().text_in_range(whole_line_range);
100 auto left_part = whole_line_containing_match.substring(0, range.start().column());
101 auto right_part = whole_line_containing_match.substring(range.end().column(), whole_line_containing_match.length() - range.end().column());
102 StringBuilder builder;
103 builder.append(left_part);
104 builder.append(0x01);
105 builder.append(file.document().text_in_range(range));
106 builder.append(0x02);
107 builder.append(right_part);
108 matches.append({ file.name(), range, builder.to_deprecated_string() });
109 }
110 });
111
112 return adopt_ref(*new SearchResultsModel(move(matches)));
113}
114
115FindInFilesWidget::FindInFilesWidget()
116{
117 set_layout<GUI::VerticalBoxLayout>();
118
119 auto& top_container = add<Widget>();
120 top_container.set_layout<GUI::HorizontalBoxLayout>();
121 top_container.set_fixed_height(22);
122
123 m_textbox = top_container.add<GUI::TextBox>();
124
125 m_button = top_container.add<GUI::Button>("Find in files"_string.release_value_but_fixme_should_propagate_errors());
126 m_button->set_fixed_width(100);
127
128 m_result_view = add<GUI::TableView>();
129
130 m_result_view->on_activation = [](auto& index) {
131 auto& match = *(const Match*)index.internal_data();
132 open_file(match.filename);
133 current_editor().set_selection(match.range);
134 current_editor().set_focus(true);
135 };
136
137 m_button->on_click = [this](auto) {
138 auto results_model = find_in_files(m_textbox->text());
139 m_result_view->set_model(results_model);
140 };
141 m_textbox->on_return_pressed = [this] {
142 m_button->click();
143 };
144}
145
146void FindInFilesWidget::focus_textbox_and_select_all()
147{
148 m_textbox->select_all();
149 m_textbox->set_focus(true);
150}
151void FindInFilesWidget::reset()
152{
153 m_result_view->set_model(nullptr);
154}
155
156}