Skip to content

[lldb] Improve editline completion formatting #116456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lldb/include/lldb/Host/Editline.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 82 additions & 7 deletions lldb/source/Host/common/Editline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -927,12 +927,86 @@ unsigned char Editline::BufferEndCommand(int ch) {
static void
PrintCompletion(FILE *output_file,
llvm::ArrayRef<CompletionResult::Completion> 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<int>(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<int>(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<int>(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<int>(description_length),
line.data());
} else {
fprintf(output_file, "%.*s...\n",
static_cast<int>(max_length - position - ellipsis_length),
line.data());
continue;
}
}
}
}

Expand All @@ -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;
}

Expand All @@ -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;

Expand Down
64 changes: 64 additions & 0 deletions lldb/test/API/terminal/TestEditlineCompletions.py
Original file line number Diff line number Diff line change
@@ -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.")
Loading