diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h index 57e2c831e3499..e8e8a6c0d4f67 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -238,6 +238,8 @@ class Editline { /// Convert the current input lines into a UTF8 StringList StringList GetInputAsStringList(int line_count = UINT32_MAX); + size_t GetTerminalWidth() { return m_terminal_width; } + private: /// Sets the lowest line number for multi-line editing sessions. A value of /// zero suppresses line number printing in the prompt. diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index f95f854c5f220..1dc1c3fc18084 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -927,12 +927,86 @@ unsigned char Editline::BufferEndCommand(int ch) { static void PrintCompletion(FILE *output_file, llvm::ArrayRef results, - size_t max_len) { + size_t max_completion_length, size_t max_length) { + constexpr size_t ellipsis_length = 3; + constexpr size_t padding_length = 8; + constexpr size_t separator_length = 4; + + const size_t description_col = + std::min(max_completion_length + padding_length, max_length); + for (const CompletionResult::Completion &c : results) { - fprintf(output_file, "\t%-*s", (int)max_len, c.GetCompletion().c_str()); - if (!c.GetDescription().empty()) - fprintf(output_file, " -- %s", c.GetDescription().c_str()); - fprintf(output_file, "\n"); + if (c.GetCompletion().empty()) + continue; + + // Print the leading padding. + fprintf(output_file, " "); + + // Print the completion with trailing padding to the description column if + // that fits on the screen. Otherwise print whatever fits on the screen + // followed by ellipsis. + const size_t completion_length = c.GetCompletion().size(); + if (padding_length + completion_length < max_length) { + fprintf(output_file, "%-*s", + static_cast(description_col - padding_length), + c.GetCompletion().c_str()); + } else { + // If the completion doesn't fit on the screen, print ellipsis and don't + // bother with the description. + fprintf(output_file, "%.*s...\n\n", + static_cast(max_length - padding_length - ellipsis_length), + c.GetCompletion().c_str()); + continue; + } + + // If we don't have a description, or we don't have enough space left to + // print the separator followed by the ellipsis, we're done. + if (c.GetDescription().empty() || + description_col + separator_length + ellipsis_length >= max_length) { + fprintf(output_file, "\n"); + continue; + } + + // Print the separator. + fprintf(output_file, " -- "); + + // Descriptions can contain newlines. We want to print them below each + // other, aligned after the separator. For example, foo has a + // two-line description: + // + // foo -- Something that fits on the line. + // More information below. + // + // However, as soon as a line exceed the available screen width and + // print ellipsis, we don't print the next line. For example, foo has a + // three-line description: + // + // foo -- Something that fits on the line. + // Something much longer that doesn't fit... + // + // Because we had to print ellipsis on line two, we don't print the + // third line. + bool first = true; + for (llvm::StringRef line : llvm::split(c.GetDescription(), '\n')) { + if (line.empty()) + break; + if (!first) + fprintf(output_file, "%*s", + static_cast(description_col + separator_length), ""); + + first = false; + const size_t position = description_col + separator_length; + const size_t description_length = line.size(); + if (position + description_length < max_length) { + fprintf(output_file, "%.*s\n", static_cast(description_length), + line.data()); + } else { + fprintf(output_file, "%.*s...\n", + static_cast(max_length - position - ellipsis_length), + line.data()); + continue; + } + } } } @@ -953,7 +1027,8 @@ void Editline::DisplayCompletions( const size_t max_len = longest->GetCompletion().size(); if (results.size() < page_size) { - PrintCompletion(editline.m_output_file, results, max_len); + PrintCompletion(editline.m_output_file, results, max_len, + editline.GetTerminalWidth()); return; } @@ -963,7 +1038,7 @@ void Editline::DisplayCompletions( size_t next_size = all ? remaining : std::min(page_size, remaining); PrintCompletion(editline.m_output_file, results.slice(cur_pos, next_size), - max_len); + max_len, editline.GetTerminalWidth()); cur_pos += next_size; diff --git a/lldb/test/API/terminal/TestEditlineCompletions.py b/lldb/test/API/terminal/TestEditlineCompletions.py new file mode 100644 index 0000000000000..7fa6f95c130c6 --- /dev/null +++ b/lldb/test/API/terminal/TestEditlineCompletions.py @@ -0,0 +1,64 @@ +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +from lldbsuite.test.lldbpexpect import PExpectTest + + +class EditlineCompletionsTest(PExpectTest): + @skipIfAsan + @skipIfEditlineSupportMissing + def test_completion_truncated(self): + """Test that the completion is correctly truncated.""" + self.launch(dimensions=(10, 20)) + self.child.send("_regexp-\t") + self.child.expect(" _regexp-a...") + self.child.expect(" _regexp-b...") + + @skipIfAsan + @skipIfEditlineSupportMissing + def test_description_truncated(self): + """Test that the description is correctly truncated.""" + self.launch(dimensions=(10, 70)) + self.child.send("_regexp-\t") + self.child.expect( + " _regexp-attach -- Attach to process by ID or name." + ) + self.child.expect( + " _regexp-break -- Set a breakpoint using one of several..." + ) + + @skipIfAsan + @skipIfEditlineSupportMissing + def test_separator_omitted(self): + """Test that the separated is correctly omitted.""" + self.launch(dimensions=(10, 32)) + self.child.send("_regexp-\t") + self.child.expect(" _regexp-attach \r\n") + self.child.expect(" _regexp-break \r\n") + + @skipIfAsan + @skipIfEditlineSupportMissing + def test_separator(self): + """Test that the separated is correctly printed.""" + self.launch(dimensions=(10, 33)) + self.child.send("_regexp-\t") + self.child.expect(" _regexp-attach -- A...") + self.child.expect(" _regexp-break -- S...") + + @skipIfAsan + @skipIfEditlineSupportMissing + def test_multiline_description(self): + """Test that multi-line descriptions are correctly padded and truncated.""" + self.launch(dimensions=(10, 72)) + self.child.send("k\t") + self.child.expect( + " kdp-remote -- Connect to a process via remote KDP server." + ) + self.child.expect( + " If no UDP port is specified, port 41139 is assu..." + ) + self.child.expect( + " kdp-remote is an abbreviation for 'process conn..." + ) + self.child.expect(" kill -- Terminate the current target process.")