Skip to content

Commit dd78d7c

Browse files
authored
[lldb] Improve editline completion formatting (#116456)
This patch improves the formatting of editline completions. The current implementation is naive and doesn't account for the terminal width. Concretely, the old implementation suffered from the following issues: - We would unconditionally pad to the longest completion. If that completion exceeds the width of the terminal, that would result in a lot of superfluous white space and line wrapping. - When printing the description, we wouldn't account for the presence of newlines, and they would continue without leading padding. The new code accounts for both. If the completion exceeds the available terminal width, we show what fits on the current lined followed by ellipsis. We also no longer pad beyond the length of the current line. Finally, we print the description line by line, with the proper leading padding. If a line of the description exceeds the available terminal width, we print ellipsis and won't print the next line. Before: ``` Available completions: _regexp-attach -- Attach to process by ID or name. _regexp-break -- Set a breakpoint using one of several shorthand formats. _regexp-bt -- Show backtrace of the current thread's call sta ck. Any numeric argument displays at most that many frames. The argument 'al l' displays all threads. Use 'settings set frame-format' to customize the pr inting of individual frames and 'settings set thread-format' to customize th e thread header. Frame recognizers may filter thelist. Use 'thread backtrace -u (--unfiltered)' to see them all. _regexp-display -- Evaluate an expression at every stop (see 'help target stop-hook'.) ``` After: ``` Available completions: _regexp-attach -- Attach to process by ID or name. _regexp-break -- Set a breakpoint using one of several shorth... _regexp-bt -- Show backtrace of the current thread's call ... _regexp-display -- Evaluate an expression at every stop (see 'h... ``` rdar://135818198
1 parent b28eebf commit dd78d7c

File tree

3 files changed

+148
-7
lines changed

3 files changed

+148
-7
lines changed

lldb/include/lldb/Host/Editline.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ class Editline {
238238
/// Convert the current input lines into a UTF8 StringList
239239
StringList GetInputAsStringList(int line_count = UINT32_MAX);
240240

241+
size_t GetTerminalWidth() { return m_terminal_width; }
242+
241243
private:
242244
/// Sets the lowest line number for multi-line editing sessions. A value of
243245
/// zero suppresses line number printing in the prompt.

lldb/source/Host/common/Editline.cpp

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -927,12 +927,86 @@ unsigned char Editline::BufferEndCommand(int ch) {
927927
static void
928928
PrintCompletion(FILE *output_file,
929929
llvm::ArrayRef<CompletionResult::Completion> results,
930-
size_t max_len) {
930+
size_t max_completion_length, size_t max_length) {
931+
constexpr size_t ellipsis_length = 3;
932+
constexpr size_t padding_length = 8;
933+
constexpr size_t separator_length = 4;
934+
935+
const size_t description_col =
936+
std::min(max_completion_length + padding_length, max_length);
937+
931938
for (const CompletionResult::Completion &c : results) {
932-
fprintf(output_file, "\t%-*s", (int)max_len, c.GetCompletion().c_str());
933-
if (!c.GetDescription().empty())
934-
fprintf(output_file, " -- %s", c.GetDescription().c_str());
935-
fprintf(output_file, "\n");
939+
if (c.GetCompletion().empty())
940+
continue;
941+
942+
// Print the leading padding.
943+
fprintf(output_file, " ");
944+
945+
// Print the completion with trailing padding to the description column if
946+
// that fits on the screen. Otherwise print whatever fits on the screen
947+
// followed by ellipsis.
948+
const size_t completion_length = c.GetCompletion().size();
949+
if (padding_length + completion_length < max_length) {
950+
fprintf(output_file, "%-*s",
951+
static_cast<int>(description_col - padding_length),
952+
c.GetCompletion().c_str());
953+
} else {
954+
// If the completion doesn't fit on the screen, print ellipsis and don't
955+
// bother with the description.
956+
fprintf(output_file, "%.*s...\n\n",
957+
static_cast<int>(max_length - padding_length - ellipsis_length),
958+
c.GetCompletion().c_str());
959+
continue;
960+
}
961+
962+
// If we don't have a description, or we don't have enough space left to
963+
// print the separator followed by the ellipsis, we're done.
964+
if (c.GetDescription().empty() ||
965+
description_col + separator_length + ellipsis_length >= max_length) {
966+
fprintf(output_file, "\n");
967+
continue;
968+
}
969+
970+
// Print the separator.
971+
fprintf(output_file, " -- ");
972+
973+
// Descriptions can contain newlines. We want to print them below each
974+
// other, aligned after the separator. For example, foo has a
975+
// two-line description:
976+
//
977+
// foo -- Something that fits on the line.
978+
// More information below.
979+
//
980+
// However, as soon as a line exceed the available screen width and
981+
// print ellipsis, we don't print the next line. For example, foo has a
982+
// three-line description:
983+
//
984+
// foo -- Something that fits on the line.
985+
// Something much longer that doesn't fit...
986+
//
987+
// Because we had to print ellipsis on line two, we don't print the
988+
// third line.
989+
bool first = true;
990+
for (llvm::StringRef line : llvm::split(c.GetDescription(), '\n')) {
991+
if (line.empty())
992+
break;
993+
if (!first)
994+
fprintf(output_file, "%*s",
995+
static_cast<int>(description_col + separator_length), "");
996+
997+
first = false;
998+
const size_t position = description_col + separator_length;
999+
const size_t description_length = line.size();
1000+
if (position + description_length < max_length) {
1001+
fprintf(output_file, "%.*s\n", static_cast<int>(description_length),
1002+
line.data());
1003+
} else {
1004+
fprintf(output_file, "%.*s...\n",
1005+
static_cast<int>(max_length - position - ellipsis_length),
1006+
line.data());
1007+
continue;
1008+
}
1009+
}
9361010
}
9371011
}
9381012

@@ -953,7 +1027,8 @@ void Editline::DisplayCompletions(
9531027
const size_t max_len = longest->GetCompletion().size();
9541028

9551029
if (results.size() < page_size) {
956-
PrintCompletion(editline.m_output_file, results, max_len);
1030+
PrintCompletion(editline.m_output_file, results, max_len,
1031+
editline.GetTerminalWidth());
9571032
return;
9581033
}
9591034

@@ -963,7 +1038,7 @@ void Editline::DisplayCompletions(
9631038
size_t next_size = all ? remaining : std::min(page_size, remaining);
9641039

9651040
PrintCompletion(editline.m_output_file, results.slice(cur_pos, next_size),
966-
max_len);
1041+
max_len, editline.GetTerminalWidth());
9671042

9681043
cur_pos += next_size;
9691044

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import lldb
2+
from lldbsuite.test.decorators import *
3+
from lldbsuite.test.lldbtest import *
4+
from lldbsuite.test import lldbutil
5+
from lldbsuite.test.lldbpexpect import PExpectTest
6+
7+
8+
class EditlineCompletionsTest(PExpectTest):
9+
@skipIfAsan
10+
@skipIfEditlineSupportMissing
11+
def test_completion_truncated(self):
12+
"""Test that the completion is correctly truncated."""
13+
self.launch(dimensions=(10, 20))
14+
self.child.send("_regexp-\t")
15+
self.child.expect(" _regexp-a...")
16+
self.child.expect(" _regexp-b...")
17+
18+
@skipIfAsan
19+
@skipIfEditlineSupportMissing
20+
def test_description_truncated(self):
21+
"""Test that the description is correctly truncated."""
22+
self.launch(dimensions=(10, 70))
23+
self.child.send("_regexp-\t")
24+
self.child.expect(
25+
" _regexp-attach -- Attach to process by ID or name."
26+
)
27+
self.child.expect(
28+
" _regexp-break -- Set a breakpoint using one of several..."
29+
)
30+
31+
@skipIfAsan
32+
@skipIfEditlineSupportMissing
33+
def test_separator_omitted(self):
34+
"""Test that the separated is correctly omitted."""
35+
self.launch(dimensions=(10, 32))
36+
self.child.send("_regexp-\t")
37+
self.child.expect(" _regexp-attach \r\n")
38+
self.child.expect(" _regexp-break \r\n")
39+
40+
@skipIfAsan
41+
@skipIfEditlineSupportMissing
42+
def test_separator(self):
43+
"""Test that the separated is correctly printed."""
44+
self.launch(dimensions=(10, 33))
45+
self.child.send("_regexp-\t")
46+
self.child.expect(" _regexp-attach -- A...")
47+
self.child.expect(" _regexp-break -- S...")
48+
49+
@skipIfAsan
50+
@skipIfEditlineSupportMissing
51+
def test_multiline_description(self):
52+
"""Test that multi-line descriptions are correctly padded and truncated."""
53+
self.launch(dimensions=(10, 72))
54+
self.child.send("k\t")
55+
self.child.expect(
56+
" kdp-remote -- Connect to a process via remote KDP server."
57+
)
58+
self.child.expect(
59+
" If no UDP port is specified, port 41139 is assu..."
60+
)
61+
self.child.expect(
62+
" kdp-remote is an abbreviation for 'process conn..."
63+
)
64+
self.child.expect(" kill -- Terminate the current target process.")

0 commit comments

Comments
 (0)