/src/serenity/Userland/Libraries/LibLine/XtermSuggestionDisplay.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020-2021, the SerenityOS developers. |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/BinarySearch.h> |
8 | | #include <AK/Function.h> |
9 | | #include <AK/StringBuilder.h> |
10 | | #include <LibCore/File.h> |
11 | | #include <LibLine/SuggestionDisplay.h> |
12 | | #include <LibLine/VT.h> |
13 | | #include <stdio.h> |
14 | | |
15 | | namespace Line { |
16 | | |
17 | | ErrorOr<void> XtermSuggestionDisplay::display(SuggestionManager const& manager) |
18 | 0 | { |
19 | 0 | did_display(); |
20 | |
|
21 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
22 | |
|
23 | 0 | size_t longest_suggestion_length = 0; |
24 | 0 | size_t longest_suggestion_byte_length = 0; |
25 | 0 | size_t longest_suggestion_byte_length_without_trivia = 0; |
26 | |
|
27 | 0 | manager.set_start_index(0); |
28 | 0 | TRY(manager.for_each_suggestion([&](auto& suggestion, auto) { |
29 | 0 | longest_suggestion_length = max(longest_suggestion_length, suggestion.text_view().length() + suggestion.display_trivia_view().length()); |
30 | 0 | longest_suggestion_byte_length = max(longest_suggestion_byte_length, suggestion.text_string().length() + suggestion.display_trivia_string().length()); |
31 | 0 | longest_suggestion_byte_length_without_trivia = max(longest_suggestion_byte_length_without_trivia, suggestion.text_string().length()); |
32 | 0 | return IterationDecision::Continue; |
33 | 0 | })); |
34 | |
|
35 | 0 | size_t num_printed = 0; |
36 | 0 | size_t lines_used = 1; |
37 | |
|
38 | 0 | TRY(VT::save_cursor(*stderr_stream)); |
39 | 0 | TRY(VT::clear_lines(0, m_lines_used_for_last_suggestions, *stderr_stream)); |
40 | 0 | TRY(VT::restore_cursor(*stderr_stream)); |
41 | |
|
42 | 0 | auto spans_entire_line { false }; |
43 | 0 | Vector<StringMetrics::LineMetrics> lines; |
44 | 0 | for (size_t i = 0; i < m_prompt_lines_at_suggestion_initiation - 1; ++i) |
45 | 0 | lines.append({ {}, 0 }); |
46 | 0 | lines.append({ {}, longest_suggestion_length }); |
47 | 0 | auto max_line_count = StringMetrics { move(lines) }.lines_with_addition({ { { {}, 0 } } }, m_num_columns); |
48 | 0 | if (longest_suggestion_length >= m_num_columns - 2) { |
49 | 0 | spans_entire_line = true; |
50 | | // We should make enough space for the biggest entry in |
51 | | // the suggestion list to fit in the prompt line. |
52 | 0 | auto start = max_line_count - m_prompt_lines_at_suggestion_initiation; |
53 | 0 | for (size_t i = start; i < max_line_count; ++i) |
54 | 0 | TRY(stderr_stream->write_until_depleted("\n"sv.bytes())); |
55 | 0 | lines_used += max_line_count; |
56 | 0 | longest_suggestion_length = 0; |
57 | 0 | } |
58 | |
|
59 | 0 | TRY(VT::move_absolute(max_line_count + m_origin_row, 1, *stderr_stream)); |
60 | |
|
61 | 0 | if (m_pages.is_empty()) { |
62 | 0 | size_t num_printed = 0; |
63 | 0 | size_t lines_used = 1; |
64 | | // Cache the pages. |
65 | 0 | manager.set_start_index(0); |
66 | 0 | size_t page_start = 0; |
67 | 0 | TRY(manager.for_each_suggestion([&](auto& suggestion, auto index) { |
68 | 0 | size_t next_column = num_printed + suggestion.text_view().length() + longest_suggestion_length + 2; |
69 | 0 | if (next_column > m_num_columns) { |
70 | 0 | auto lines = (suggestion.text_view().length() + m_num_columns - 1) / m_num_columns; |
71 | 0 | lines_used += lines; |
72 | 0 | num_printed = 0; |
73 | 0 | } |
74 | |
|
75 | 0 | if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines) { |
76 | 0 | m_pages.append({ page_start, index }); |
77 | 0 | page_start = index; |
78 | 0 | lines_used = 1; |
79 | 0 | num_printed = 0; |
80 | 0 | } |
81 | |
|
82 | 0 | if (spans_entire_line) |
83 | 0 | num_printed += m_num_columns; |
84 | 0 | else |
85 | 0 | num_printed += longest_suggestion_length + 2; |
86 | |
|
87 | 0 | return IterationDecision::Continue; |
88 | 0 | })); |
89 | | // Append the last page. |
90 | 0 | m_pages.append({ page_start, manager.count() }); |
91 | 0 | } |
92 | |
|
93 | 0 | auto page_index = fit_to_page_boundary(manager.next_index()); |
94 | |
|
95 | 0 | manager.set_start_index(m_pages[page_index].start); |
96 | 0 | TRY(manager.for_each_suggestion([&](auto& suggestion, auto index) -> ErrorOr<IterationDecision> { |
97 | 0 | size_t next_column = num_printed + suggestion.text_view().length() + longest_suggestion_length + 2; |
98 | |
|
99 | 0 | if (next_column > m_num_columns) { |
100 | 0 | auto lines = (suggestion.text_view().length() + m_num_columns - 1) / m_num_columns; |
101 | 0 | lines_used += lines; |
102 | 0 | TRY(stderr_stream->write_until_depleted("\n"sv.bytes())); |
103 | 0 | num_printed = 0; |
104 | 0 | } |
105 | | |
106 | | // Show just enough suggestions to fill up the screen |
107 | | // without moving the prompt out of view. |
108 | 0 | if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines) |
109 | 0 | return IterationDecision::Break; |
110 | | |
111 | | // Only apply color to the selection if something is *actually* added to the buffer. |
112 | 0 | if (manager.is_current_suggestion_complete() && index == manager.next_index()) { |
113 | 0 | TRY(VT::apply_style({ Style::Foreground(Style::XtermColor::Blue) }, *stderr_stream)); |
114 | 0 | } |
115 | |
|
116 | 0 | if (spans_entire_line) { |
117 | 0 | num_printed += m_num_columns; |
118 | 0 | TRY(stderr_stream->write_until_depleted(suggestion.text_string().bytes())); |
119 | 0 | TRY(stderr_stream->write_until_depleted(suggestion.display_trivia_string().bytes())); |
120 | 0 | } else { |
121 | 0 | auto field = ByteString::formatted("{: <{}} {}", suggestion.text_string(), longest_suggestion_byte_length_without_trivia, suggestion.display_trivia_string()); |
122 | 0 | TRY(stderr_stream->write_until_depleted(ByteString::formatted("{: <{}}", field, longest_suggestion_byte_length + 2))); |
123 | 0 | num_printed += longest_suggestion_length + 2; |
124 | 0 | } |
125 | |
|
126 | 0 | if (manager.is_current_suggestion_complete() && index == manager.next_index()) |
127 | 0 | TRY(VT::apply_style(Style::reset_style(), *stderr_stream)); |
128 | 0 | return IterationDecision::Continue; |
129 | 0 | })); |
130 | |
|
131 | 0 | m_lines_used_for_last_suggestions = lines_used; |
132 | | |
133 | | // The last line of the prompt is the same line as the first line of the buffer, so we need to subtract one here. |
134 | 0 | lines_used += m_prompt_lines_at_suggestion_initiation - 1; |
135 | | |
136 | | // If we filled the screen, move back the origin. |
137 | 0 | if (m_origin_row + lines_used >= m_num_lines) { |
138 | 0 | m_origin_row = m_num_lines - lines_used; |
139 | 0 | } |
140 | |
|
141 | 0 | if (m_pages.size() > 1) { |
142 | 0 | auto left_arrow = page_index > 0 ? '<' : ' '; |
143 | 0 | auto right_arrow = page_index < m_pages.size() - 1 ? '>' : ' '; |
144 | 0 | auto string = ByteString::formatted("{:c} page {} of {} {:c}", left_arrow, page_index + 1, m_pages.size(), right_arrow); |
145 | |
|
146 | 0 | if (string.length() > m_num_columns - 1) { |
147 | | // This would overflow into the next line, so just don't print an indicator. |
148 | 0 | return {}; |
149 | 0 | } |
150 | | |
151 | 0 | TRY(VT::move_absolute(m_origin_row + lines_used, m_num_columns - string.length() - 1, *stderr_stream)); |
152 | 0 | TRY(VT::apply_style({ Style::Background(Style::XtermColor::Green) }, *stderr_stream)); |
153 | 0 | TRY(stderr_stream->write_until_depleted(string.bytes())); |
154 | 0 | TRY(VT::apply_style(Style::reset_style(), *stderr_stream)); |
155 | 0 | } |
156 | | |
157 | 0 | return {}; |
158 | 0 | } |
159 | | |
160 | | ErrorOr<bool> XtermSuggestionDisplay::cleanup() |
161 | 0 | { |
162 | 0 | did_cleanup(); |
163 | |
|
164 | 0 | if (m_lines_used_for_last_suggestions) { |
165 | 0 | auto stderr_stream = TRY(Core::File::standard_error()); |
166 | 0 | TRY(VT::clear_lines(0, m_lines_used_for_last_suggestions, *stderr_stream)); |
167 | 0 | m_lines_used_for_last_suggestions = 0; |
168 | 0 | return true; |
169 | 0 | } |
170 | | |
171 | 0 | return false; |
172 | 0 | } |
173 | | |
174 | | size_t XtermSuggestionDisplay::fit_to_page_boundary(size_t selection_index) |
175 | 0 | { |
176 | 0 | VERIFY(m_pages.size() > 0); |
177 | 0 | size_t index = 0; |
178 | |
|
179 | 0 | auto* match = binary_search( |
180 | 0 | m_pages.span(), |
181 | 0 | PageRange { selection_index, selection_index }, |
182 | 0 | &index, |
183 | 0 | [](auto& a, auto& b) -> int { |
184 | 0 | if (a.start >= b.start && a.start < b.end) |
185 | 0 | return 0; |
186 | 0 | return a.start - b.start; |
187 | 0 | }); |
188 | |
|
189 | 0 | if (!match) |
190 | 0 | return m_pages.size() - 1; |
191 | | |
192 | 0 | return index; |
193 | 0 | } |
194 | | |
195 | | } |