/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 | | } |