LibLine: Allow suggestions to have trailing trivia strings

These strings would be applied when inserted into the buffer, but are
not shown as part of the suggestion.

This commit also patches up Userland/js and Shell to use this
functionality
This commit is contained in:
AnotherTest 2020-04-19 18:32:28 +04:30 committed by Andreas Kling
parent 7ef48171ce
commit cb3cf589ed
4 changed files with 72 additions and 70 deletions

View file

@ -299,17 +299,17 @@ String Editor::get_line(const String& prompt)
m_suggestions = on_tab_complete_other_token(token); m_suggestions = on_tab_complete_other_token(token);
size_t common_suggestion_prefix { 0 }; size_t common_suggestion_prefix { 0 };
if (m_suggestions.size() == 1) { if (m_suggestions.size() == 1) {
m_largest_common_suggestion_prefix_length = m_suggestions[0].length(); m_largest_common_suggestion_prefix_length = m_suggestions[0].text.length();
} else if (m_suggestions.size()) { } else if (m_suggestions.size()) {
char last_valid_suggestion_char; char last_valid_suggestion_char;
for (;; ++common_suggestion_prefix) { for (;; ++common_suggestion_prefix) {
if (m_suggestions[0].length() <= common_suggestion_prefix) if (m_suggestions[0].text.length() <= common_suggestion_prefix)
goto no_more_commons; goto no_more_commons;
last_valid_suggestion_char = m_suggestions[0][common_suggestion_prefix]; last_valid_suggestion_char = m_suggestions[0].text[common_suggestion_prefix];
for (const auto& suggestion : m_suggestions) { for (const auto& suggestion : m_suggestions) {
if (suggestion.length() < common_suggestion_prefix || suggestion[common_suggestion_prefix] != last_valid_suggestion_char) { if (suggestion.text.length() < common_suggestion_prefix || suggestion.text[common_suggestion_prefix] != last_valid_suggestion_char) {
goto no_more_commons; goto no_more_commons;
} }
} }
@ -341,7 +341,7 @@ String Editor::get_line(const String& prompt)
auto current_suggestion_index = m_next_suggestion_index; auto current_suggestion_index = m_next_suggestion_index;
if (m_next_suggestion_index < m_suggestions.size()) { if (m_next_suggestion_index < m_suggestions.size()) {
auto can_complete = m_next_suggestion_invariant_offset < m_largest_common_suggestion_prefix_length; auto can_complete = m_next_suggestion_invariant_offset < m_largest_common_suggestion_prefix_length;
if (!m_last_shown_suggestion.is_null()) { if (!m_last_shown_suggestion.text.is_null()) {
size_t actual_offset; size_t actual_offset;
size_t shown_length = m_last_shown_suggestion_display_length; size_t shown_length = m_last_shown_suggestion_display_length;
switch (m_times_tab_pressed) { switch (m_times_tab_pressed) {
@ -367,17 +367,20 @@ String Editor::get_line(const String& prompt)
m_refresh_needed = true; m_refresh_needed = true;
} }
m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; m_last_shown_suggestion = m_suggestions[m_next_suggestion_index];
m_last_shown_suggestion_display_length = m_last_shown_suggestion.length(); m_last_shown_suggestion_display_length = m_last_shown_suggestion.text.length();
m_last_shown_suggestion_was_complete = true; m_last_shown_suggestion_was_complete = true;
if (m_times_tab_pressed == 1) { if (m_times_tab_pressed == 1) {
// This is the first time, so only auto-complete *if possible* // This is the first time, so only auto-complete *if possible*
if (can_complete) { if (can_complete) {
insert(m_last_shown_suggestion.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset)); insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset));
m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length; m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length;
// do not increment the suggestion index, as the first tab should only be a *peek* // do not increment the suggestion index, as the first tab should only be a *peek*
if (m_suggestions.size() == 1) { if (m_suggestions.size() == 1) {
// if there's one suggestion, commit and forget // if there's one suggestion, commit and forget
m_times_tab_pressed = 0; m_times_tab_pressed = 0;
// add in the trivia of the last selected suggestion
insert(m_last_shown_suggestion.trailing_trivia);
m_last_shown_suggestion_display_length += m_last_shown_suggestion.trailing_trivia.length();
} }
} else { } else {
m_last_shown_suggestion_display_length = 0; m_last_shown_suggestion_display_length = 0;
@ -385,7 +388,9 @@ String Editor::get_line(const String& prompt)
++m_times_tab_pressed; ++m_times_tab_pressed;
m_last_shown_suggestion_was_complete = false; m_last_shown_suggestion_was_complete = false;
} else { } else {
insert(m_last_shown_suggestion.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.length() - m_next_suggestion_invariant_offset)); insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.text.length() - m_next_suggestion_invariant_offset));
// add in the trivia of the last selected suggestion
insert(m_last_shown_suggestion.trailing_trivia);
if (m_tab_direction == TabDirection::Forward) if (m_tab_direction == TabDirection::Forward)
increment_suggestion_index(); increment_suggestion_index();
else else
@ -399,7 +404,7 @@ String Editor::get_line(const String& prompt)
size_t longest_suggestion_length = 0; size_t longest_suggestion_length = 0;
for (auto& suggestion : m_suggestions) { for (auto& suggestion : m_suggestions) {
longest_suggestion_length = max(longest_suggestion_length, suggestion.length()); longest_suggestion_length = max(longest_suggestion_length, suggestion.text.length());
} }
size_t num_printed = 0; size_t num_printed = 0;
@ -423,10 +428,10 @@ String Editor::get_line(const String& prompt)
} }
vt_move_absolute(max_line_count + m_origin_x, 1); vt_move_absolute(max_line_count + m_origin_x, 1);
for (auto& suggestion : m_suggestions) { for (auto& suggestion : m_suggestions) {
size_t next_column = num_printed + suggestion.length() + longest_suggestion_length + 2; size_t next_column = num_printed + suggestion.text.length() + longest_suggestion_length + 2;
if (next_column > m_num_columns) { if (next_column > m_num_columns) {
auto lines = (suggestion.length() + m_num_columns - 1) / m_num_columns; auto lines = (suggestion.text.length() + m_num_columns - 1) / m_num_columns;
lines_used += lines; lines_used += lines;
putchar('\n'); putchar('\n');
num_printed = 0; num_printed = 0;
@ -445,9 +450,9 @@ String Editor::get_line(const String& prompt)
if (spans_entire_line) { if (spans_entire_line) {
num_printed += m_num_columns; num_printed += m_num_columns;
fprintf(stderr, "%s", suggestion.characters()); fprintf(stderr, "%s", suggestion.text.characters());
} else { } else {
num_printed += fprintf(stderr, "%-*s", static_cast<int>(longest_suggestion_length) + 2, suggestion.characters()); num_printed += fprintf(stderr, "%-*s", static_cast<int>(longest_suggestion_length) + 2, suggestion.text.characters());
} }
if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) { if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) {

View file

@ -53,6 +53,28 @@ struct KeyCallback {
Function<bool(Editor&)> callback; Function<bool(Editor&)> callback;
}; };
struct CompletionSuggestion {
// intentionally not explicit (allows suggesting bare strings)
CompletionSuggestion(const String& completion)
: text(completion)
, trailing_trivia("")
{
}
CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia)
: text(completion)
, trailing_trivia(trailing_trivia)
{
}
bool operator==(const CompletionSuggestion& suggestion) const
{
return suggestion.text == text;
}
String text;
String trailing_trivia;
};
class Editor { class Editor {
public: public:
Editor(); Editor();
@ -79,8 +101,8 @@ public:
void register_character_input_callback(char ch, Function<bool(Editor&)> callback); void register_character_input_callback(char ch, Function<bool(Editor&)> callback);
Function<Vector<String>(const String&)> on_tab_complete_first_token; Function<Vector<CompletionSuggestion>(const String&)> on_tab_complete_first_token;
Function<Vector<String>(const String&)> on_tab_complete_other_token; Function<Vector<CompletionSuggestion>(const String&)> on_tab_complete_other_token;
Function<void(Editor&)> on_display_refresh; Function<void(Editor&)> on_display_refresh;
// FIXME: we will have to kindly ask our instantiators to set our signal handlers // FIXME: we will have to kindly ask our instantiators to set our signal handlers
@ -195,8 +217,8 @@ private:
size_t m_origin_y { 0 }; size_t m_origin_y { 0 };
String m_new_prompt; String m_new_prompt;
Vector<String> m_suggestions; Vector<CompletionSuggestion> m_suggestions;
String m_last_shown_suggestion { String::empty() }; CompletionSuggestion m_last_shown_suggestion { String::empty() };
size_t m_last_shown_suggestion_display_length { 0 }; size_t m_last_shown_suggestion_display_length { 0 };
bool m_last_shown_suggestion_was_complete { false }; bool m_last_shown_suggestion_was_complete { false };
size_t m_next_suggestion_index { 0 }; size_t m_next_suggestion_index { 0 };

View file

@ -1042,7 +1042,7 @@ int main(int argc, char** argv)
g.termios = editor.termios(); g.termios = editor.termios();
g.default_termios = editor.default_termios(); g.default_termios = editor.default_termios();
editor.on_tab_complete_first_token = [&](const String& token) -> Vector<String> { editor.on_tab_complete_first_token = [&](const String& token) -> Vector<Line::CompletionSuggestion> {
auto match = binary_search(cached_path.data(), cached_path.size(), token, [](const String& token, const String& program) -> int { auto match = binary_search(cached_path.data(), cached_path.size(), token, [](const String& token, const String& program) -> int {
return strncmp(token.characters(), program.characters(), token.length()); return strncmp(token.characters(), program.characters(), token.length());
}); });
@ -1052,7 +1052,7 @@ int main(int argc, char** argv)
// Suggest local executables and directories // Suggest local executables and directories
auto mut_token = token; // copy it :( auto mut_token = token; // copy it :(
String path; String path;
Vector<String> local_suggestions; Vector<Line::CompletionSuggestion> local_suggestions;
bool suggest_executables = true; bool suggest_executables = true;
ssize_t last_slash = token.length() - 1; ssize_t last_slash = token.length() - 1;
@ -1090,6 +1090,7 @@ int main(int argc, char** argv)
// manually skip `.' and `..' // manually skip `.' and `..'
if (file == "." || file == "..") if (file == "." || file == "..")
continue; continue;
auto trivia = " ";
if (file.starts_with(mut_token)) { if (file.starts_with(mut_token)) {
String file_path = String::format("%s/%s", path.characters(), file.characters()); String file_path = String::format("%s/%s", path.characters(), file.characters());
struct stat program_status; struct stat program_status;
@ -1098,25 +1099,14 @@ int main(int argc, char** argv)
continue; continue;
if (!(program_status.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) if (!(program_status.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
continue; continue;
if (!S_ISDIR(program_status.st_mode) && !suggest_executables) if (S_ISDIR(program_status.st_mode)) {
continue; if (suggest_executables)
continue;
else
trivia = "/";
}
local_suggestions.append(file); local_suggestions.append({ file, trivia });
}
}
// If we have a single match and it's a directory, we add a slash. If it's
// a regular file, we add a space, unless we already have one.
if (local_suggestions.size() == 1) {
auto& completion = local_suggestions[0];
String file_path = String::format("%s/%s", path.characters(), completion.characters());
struct stat program_status;
int stat_error = stat(file_path.characters(), &program_status);
if (!stat_error) {
if (S_ISDIR(program_status.st_mode))
completion = String::format("%s/", local_suggestions[0].characters());
else if (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')
completion = String::format("%s ", local_suggestions[0].characters());
} }
} }
@ -1124,37 +1114,29 @@ int main(int argc, char** argv)
} }
String completion = *match; String completion = *match;
Vector<String> suggestions; Vector<Line::CompletionSuggestion> suggestions;
// Now that we have a program name starting with our token, we look at // Now that we have a program name starting with our token, we look at
// other program names starting with our token and cut off any mismatching // other program names starting with our token and cut off any mismatching
// characters. // characters.
bool seen_others = false;
int index = match - cached_path.data(); int index = match - cached_path.data();
for (int i = index - 1; i >= 0 && cached_path[i].starts_with(token); --i) { for (int i = index - 1; i >= 0 && cached_path[i].starts_with(token); --i) {
suggestions.append(cached_path[i]); suggestions.append({ cached_path[i], " " });
seen_others = true;
} }
for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) { for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) {
suggestions.append(cached_path[i]); suggestions.append({ cached_path[i], " " });
seen_others = true;
}
suggestions.append(cached_path[index]);
// If we have a single match, we add a space, unless we already have one.
if (!seen_others && (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')) {
suggestions[0] = String::format("%s ", suggestions[0].characters());
} }
suggestions.append({ cached_path[index], " " });
editor.suggest(token.length(), 0); editor.suggest(token.length(), 0);
return suggestions; return suggestions;
}; };
editor.on_tab_complete_other_token = [&](const String& vtoken) -> Vector<String> { editor.on_tab_complete_other_token = [&](const String& vtoken) -> Vector<Line::CompletionSuggestion> {
auto token = vtoken; // copy it :( auto token = vtoken; // copy it :(
String path; String path;
Vector<String> suggestions; Vector<Line::CompletionSuggestion> suggestions;
ssize_t last_slash = token.length() - 1; ssize_t last_slash = token.length() - 1;
while (last_slash >= 0 && token[last_slash] != '/') while (last_slash >= 0 && token[last_slash] != '/')
@ -1190,22 +1172,15 @@ int main(int argc, char** argv)
if (file == "." || file == "..") if (file == "." || file == "..")
continue; continue;
if (file.starts_with(token)) { if (file.starts_with(token)) {
suggestions.append(file); struct stat program_status;
} String file_path = String::format("%s/%s", path.characters(), file.characters());
} int stat_error = stat(file_path.characters(), &program_status);
if (!stat_error) {
// If we have a single match and it's a directory, we add a slash. If it's if (S_ISDIR(program_status.st_mode))
// a regular file, we add a space, unless we already have one. suggestions.append({ file, "/" });
if (suggestions.size() == 1) { else
auto& completion = suggestions[0]; suggestions.append({ file, " " });
String file_path = String::format("%s/%s", path.characters(), completion.characters()); }
struct stat program_status;
int stat_error = stat(file_path.characters(), &program_status);
if (!stat_error) {
if (S_ISDIR(program_status.st_mode))
completion = String::format("%s/", suggestions[0].characters());
else if (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')
completion = String::format("%s ", suggestions[0].characters());
} }
} }

View file

@ -553,7 +553,7 @@ int main(int argc, char** argv)
editor.set_prompt(prompt_for_level(open_indents)); editor.set_prompt(prompt_for_level(open_indents));
}; };
auto complete = [&interpreter, &editor = *editor](const String& token) -> Vector<String> { auto complete = [&interpreter, &editor = *editor](const String& token) -> Vector<Line::CompletionSuggestion> {
if (token.length() == 0) if (token.length() == 0)
return {}; // nyeh return {}; // nyeh
@ -564,13 +564,13 @@ int main(int argc, char** argv)
// - <N>.<P> // - <N>.<P>
// where N is the complete name of a variable and // where N is the complete name of a variable and
// P is part of the name of one of its properties // P is part of the name of one of its properties
Vector<String> results; Vector<Line::CompletionSuggestion> results;
Function<void(const JS::Shape&, const StringView&)> list_all_properties = [&results, &list_all_properties](const JS::Shape& shape, auto& property_pattern) { Function<void(const JS::Shape&, const StringView&)> list_all_properties = [&results, &list_all_properties](const JS::Shape& shape, auto& property_pattern) {
for (const auto& descriptor : shape.property_table()) { for (const auto& descriptor : shape.property_table()) {
if (descriptor.value.attributes & JS::Attribute::Enumerable) { if (descriptor.value.attributes & JS::Attribute::Enumerable) {
if (descriptor.key.view().starts_with(property_pattern)) { if (descriptor.key.view().starts_with(property_pattern)) {
auto completion = descriptor.key; Line::CompletionSuggestion completion { descriptor.key };
if (!results.contains_slow(completion)) { // hide duplicates if (!results.contains_slow(completion)) { // hide duplicates
results.append(completion); results.append(completion);
} }