Coverage Report

Created: 2026-11-13 08:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}