Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibLine/SuggestionManager.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2020, the SerenityOS developers.
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/Assertions.h>
8
#include <AK/Function.h>
9
#include <LibLine/SuggestionManager.h>
10
11
namespace Line {
12
13
CompletionSuggestion::CompletionSuggestion(StringView completion, StringView trailing_trivia, StringView display_trivia, Style style)
14
0
    : text(MUST(String::from_utf8(completion)))
15
0
    , trailing_trivia(MUST(String::from_utf8(trailing_trivia)))
16
0
    , display_trivia(MUST(String::from_utf8(display_trivia)))
17
0
    , style(style)
18
0
    , is_valid(true)
19
0
{
20
0
}
21
22
void SuggestionManager::set_suggestions(Vector<CompletionSuggestion>&& suggestions)
23
0
{
24
0
    auto code_point_at = [](Utf8View view, size_t index) {
25
0
        size_t count = 0;
26
0
        for (auto cp : view) {
27
0
            if (count == index) {
28
0
                return cp;
29
0
            }
30
0
            count++;
31
0
        }
32
0
        VERIFY_NOT_REACHED();
33
0
    };
34
35
0
    m_suggestions = move(suggestions);
36
37
0
    size_t common_suggestion_prefix { 0 };
38
0
    if (m_suggestions.size() == 1) {
39
0
        m_largest_common_suggestion_prefix_length = m_suggestions[0].text_view().length();
40
0
    } else if (m_suggestions.size()) {
41
0
        u32 last_valid_suggestion_code_point;
42
43
0
        for (;; ++common_suggestion_prefix) {
44
0
            if (m_suggestions[0].text_view().length() <= common_suggestion_prefix)
45
0
                goto no_more_commons;
46
47
0
            last_valid_suggestion_code_point = code_point_at(m_suggestions[0].text_view(), common_suggestion_prefix);
48
49
0
            for (auto& suggestion : m_suggestions) {
50
0
                if (suggestion.text_view().length() <= common_suggestion_prefix || code_point_at(suggestion.text_view(), common_suggestion_prefix) != last_valid_suggestion_code_point) {
51
0
                    goto no_more_commons;
52
0
                }
53
0
            }
54
0
        }
55
0
    no_more_commons:;
56
0
        m_largest_common_suggestion_prefix_length = common_suggestion_prefix;
57
0
    } else {
58
0
        m_largest_common_suggestion_prefix_length = 0;
59
0
    }
60
0
}
61
62
void SuggestionManager::next()
63
0
{
64
0
    if (m_suggestions.size())
65
0
        m_next_suggestion_index = (m_next_suggestion_index + 1) % m_suggestions.size();
66
0
    else
67
0
        m_next_suggestion_index = 0;
68
0
}
69
70
void SuggestionManager::previous()
71
0
{
72
0
    if (m_next_suggestion_index == 0)
73
0
        m_next_suggestion_index = m_suggestions.size();
74
0
    m_next_suggestion_index--;
75
0
}
76
77
CompletionSuggestion const& SuggestionManager::suggest()
78
0
{
79
0
    auto const& suggestion = m_suggestions[m_next_suggestion_index];
80
0
    m_selected_suggestion_index = m_next_suggestion_index;
81
0
    m_last_shown_suggestion = suggestion;
82
0
    return suggestion;
83
0
}
84
85
void SuggestionManager::set_current_suggestion_initiation_index(size_t index)
86
0
{
87
0
    auto& suggestion = m_suggestions[m_next_suggestion_index];
88
89
0
    if (m_last_shown_suggestion_display_length)
90
0
        m_last_shown_suggestion.start_index = index - suggestion.static_offset - m_last_shown_suggestion_display_length;
91
0
    else
92
0
        m_last_shown_suggestion.start_index = index - suggestion.static_offset - suggestion.invariant_offset;
93
94
0
    m_last_shown_suggestion_display_length = m_last_shown_suggestion.text_view().length();
95
0
    m_last_shown_suggestion_was_complete = true;
96
0
}
97
98
SuggestionManager::CompletionAttemptResult SuggestionManager::attempt_completion(CompletionMode mode, size_t initiation_start_index)
99
0
{
100
0
    CompletionAttemptResult result { mode };
101
102
0
    if (m_next_suggestion_index < m_suggestions.size()) {
103
0
        auto& next_suggestion = m_suggestions[m_next_suggestion_index];
104
105
0
        if (mode == CompletePrefix && !next_suggestion.allow_commit_without_listing) {
106
0
            result.new_completion_mode = CompletionMode::ShowSuggestions;
107
0
            result.avoid_committing_to_single_suggestion = true;
108
0
            m_last_shown_suggestion_display_length = 0;
109
0
            m_last_shown_suggestion_was_complete = false;
110
0
            m_last_shown_suggestion = ByteString::empty();
111
0
            return result;
112
0
        }
113
114
0
        auto can_complete = next_suggestion.invariant_offset <= m_largest_common_suggestion_prefix_length;
115
0
        ssize_t actual_offset;
116
0
        size_t shown_length = m_last_shown_suggestion_display_length;
117
0
        switch (mode) {
118
0
        case CompletePrefix:
119
0
            actual_offset = 0;
120
0
            break;
121
0
        case ShowSuggestions:
122
0
            actual_offset = 0 - m_largest_common_suggestion_prefix_length + next_suggestion.invariant_offset;
123
0
            if (can_complete && next_suggestion.allow_commit_without_listing)
124
0
                shown_length = m_largest_common_suggestion_prefix_length + m_last_shown_suggestion.trivia_view().length();
125
0
            break;
126
0
        default:
127
0
            if (m_last_shown_suggestion_display_length == 0)
128
0
                actual_offset = 0;
129
0
            else
130
0
                actual_offset = 0 - m_last_shown_suggestion_display_length + next_suggestion.invariant_offset;
131
0
            break;
132
0
        }
133
134
0
        auto& suggestion = suggest();
135
0
        set_current_suggestion_initiation_index(initiation_start_index);
136
137
0
        result.offset_region_to_remove = { next_suggestion.invariant_offset, shown_length };
138
0
        result.new_cursor_offset = actual_offset;
139
0
        result.static_offset_from_cursor = next_suggestion.static_offset;
140
141
0
        if (mode == CompletePrefix) {
142
            // Only auto-complete *if possible*.
143
0
            if (can_complete) {
144
0
                result.insert.append(suggestion.text_view().unicode_substring_view(suggestion.invariant_offset, m_largest_common_suggestion_prefix_length - suggestion.invariant_offset));
145
0
                m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length;
146
                // Do not increment the suggestion index, as the first tab should only be a *peek*.
147
0
                if (m_suggestions.size() == 1) {
148
                    // If there's one suggestion, commit and forget.
149
0
                    result.new_completion_mode = DontComplete;
150
                    // Add in the trivia of the last selected suggestion.
151
0
                    result.insert.append(suggestion.trailing_trivia.code_points());
152
0
                    m_last_shown_suggestion_display_length = 0;
153
0
                    result.style_to_apply = suggestion.style;
154
0
                    m_last_shown_suggestion_was_complete = true;
155
0
                    return result;
156
0
                }
157
0
            } else {
158
0
                m_last_shown_suggestion_display_length = 0;
159
0
            }
160
0
            result.new_completion_mode = CompletionMode::ShowSuggestions;
161
0
            m_last_shown_suggestion_was_complete = false;
162
0
            m_last_shown_suggestion = ByteString::empty();
163
0
        } else {
164
0
            result.insert.append(suggestion.text_view().unicode_substring_view(suggestion.invariant_offset, suggestion.text_view().length() - suggestion.invariant_offset));
165
            // Add in the trivia of the last selected suggestion.
166
0
            result.insert.append(suggestion.trailing_trivia.code_points());
167
0
            m_last_shown_suggestion_display_length += suggestion.trivia_view().length();
168
0
        }
169
0
    } else {
170
0
        m_next_suggestion_index = 0;
171
0
    }
172
0
    return result;
173
0
}
174
175
ErrorOr<size_t> SuggestionManager::for_each_suggestion(Function<ErrorOr<IterationDecision>(CompletionSuggestion const&, size_t)> callback) const
176
0
{
177
0
    size_t start_index { 0 };
178
0
    for (auto& suggestion : m_suggestions) {
179
0
        if (start_index++ < m_last_displayed_suggestion_index)
180
0
            continue;
181
0
        if (TRY(callback(suggestion, start_index - 1)) == IterationDecision::Break)
182
0
            break;
183
0
    }
184
0
    return start_index;
185
0
}
186
187
}