https://github.com/Teemperor created https://github.com/llvm/llvm-project/pull/208677
When showing an inline autosuggestion, this now also shows the description of the completion next to it. This allows users to see what a command they are about about to tab-complete actually does. assisted-by: claude >From e445b34ec0edcd9f1ab73b99ed476b202f25324e Mon Sep 17 00:00:00 2001 From: Raphael Isemann <[email protected]> Date: Fri, 10 Jul 2026 10:47:05 +0100 Subject: [PATCH] [lldb] Show completion description in show-autosuggestion mode When showing an inline autosuggestion, this now also shows the description of the completion next to it. This allows users to see what a command they are about about to tab-complete actually does. assisted-by: claude --- lldb/include/lldb/Core/IOHandler.h | 3 +- lldb/include/lldb/Host/Editline.h | 13 +++- lldb/source/Core/IOHandler.cpp | 21 ++++-- lldb/source/Host/common/Editline.cpp | 56 +++++++++++++--- .../autosuggestion/TestAutosuggestion.py | 64 +++++++++++++++++-- 5 files changed, 135 insertions(+), 22 deletions(-) diff --git a/lldb/include/lldb/Core/IOHandler.h b/lldb/include/lldb/Core/IOHandler.h index 2672bbe5da2b3..3f4b80f23f8b1 100644 --- a/lldb/include/lldb/Core/IOHandler.h +++ b/lldb/include/lldb/Core/IOHandler.h @@ -415,7 +415,8 @@ class IOHandlerEditline : public IOHandler { int FixIndentationCallback(Editline *editline, const StringList &lines, int cursor_position); - std::optional<std::string> SuggestionCallback(llvm::StringRef line); + std::optional<std::string> SuggestionCallback(llvm::StringRef line, + std::string &description); void AutoCompleteCallback(CompletionRequest &request); diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h index 39e2c6b73eef6..51e562b76491d 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -97,8 +97,17 @@ using IsInputCompleteCallbackType = using FixIndentationCallbackType = llvm::unique_function<int(Editline *, StringList &, int)>; -using SuggestionCallbackType = - llvm::unique_function<std::optional<std::string>(llvm::StringRef)>; +/// Computes the inline autosuggestion for the given \p line. +/// +/// Returns the text that should be appended to \p line as a grayed-out inline +/// suggestion, or std::nullopt if there is no suggestion. When a suggestion is +/// returned, \p description is filled with an optional human-readable +/// description of the suggestion (for example, what tab completion would +/// insert). The description is only displayed to the user in parentheses after +/// the suggestion and is never inserted into the line when the suggestion is +/// accepted. +using SuggestionCallbackType = llvm::unique_function<std::optional<std::string>( + llvm::StringRef line, std::string &description)>; using CompleteCallbackType = llvm::unique_function<void(CompletionRequest &)>; diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp index f19fe3204ddf5..febfd9e42552a 100644 --- a/lldb/source/Core/IOHandler.cpp +++ b/lldb/source/Core/IOHandler.cpp @@ -267,9 +267,10 @@ IOHandlerEditline::IOHandlerEditline( m_editline_up->SetRedrawCallback([this]() { this->RedrawCallback(); }); if (debugger.GetAutosuggestionMode() != eAutosuggestionOff) { - m_editline_up->SetSuggestionCallback([this](llvm::StringRef line) { - return this->SuggestionCallback(line); - }); + m_editline_up->SetSuggestionCallback( + [this](llvm::StringRef line, std::string &description) { + return this->SuggestionCallback(line, description); + }); m_editline_up->SetSuggestionAnsiPrefix(ansi::FormatAnsiTerminalCodes( debugger.GetAutosuggestionAnsiPrefix())); m_editline_up->SetSuggestionAnsiSuffix(ansi::FormatAnsiTerminalCodes( @@ -439,7 +440,8 @@ int IOHandlerEditline::FixIndentationCallback(Editline *editline, } std::optional<std::string> -IOHandlerEditline::SuggestionCallback(llvm::StringRef line) { +IOHandlerEditline::SuggestionCallback(llvm::StringRef line, + std::string &description) { // In tab-mode, we just display what tab would complete if the user // would tab. if (m_debugger.GetAutosuggestionMode() == eAutosuggestionTabMode) { @@ -450,8 +452,17 @@ IOHandlerEditline::SuggestionCallback(llvm::StringRef line) { result.GetMatches(matches); std::string longest_prefix = matches.LongestCommonPrefix(); llvm::StringRef cursor_arg_prefix = request.GetCursorArgumentPrefix(); - if (longest_prefix.size() > cursor_arg_prefix.size()) + if (longest_prefix.size() > cursor_arg_prefix.size()) { + // When the suggestion corresponds to a single completion, also surface + // its description (if any) so the user can see what the completion means. + if (result.GetNumberOfResults() == 1) { + StringList descriptions; + result.GetDescriptions(descriptions); + if (descriptions.GetSize() == 1) + description = descriptions[0]; + } return longest_prefix.substr(cursor_arg_prefix.size()); + } return std::nullopt; } return m_delegate.IOHandlerSuggestion(*this, line); diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index 5be0450cb17e9..1017014307781 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -1157,6 +1157,14 @@ unsigned char Editline::TabCommand(int ch) { // completed. if (to_add == " ") return CC_REDISPLAY; + // If an autosuggestion (and its description) was drawn, it was written + // directly to the terminal and is invisible to libedit, so a plain + // refresh would leave the trailing description on screen. Force a full + // redisplay, which clears the line before redrawing. + if (m_previous_autosuggestion_size > 0) { + m_previous_autosuggestion_size = 0; + return CC_REDISPLAY; + } return CC_REFRESH; } case CompletionMode::Partial: { @@ -1200,7 +1208,10 @@ unsigned char Editline::ApplyAutosuggestCommand(int ch) { llvm::StringRef line(line_info->buffer, line_info->lastchar - line_info->buffer); - if (std::optional<std::string> to_add = m_suggestion_callback(line)) + // The description is only shown inline and never inserted into the line. + std::string description; + if (std::optional<std::string> to_add = + m_suggestion_callback(line, description)) el_insertstr(m_editline, to_add->c_str()); return CC_REDISPLAY; @@ -1218,13 +1229,45 @@ unsigned char Editline::TypedCharacter(int ch) { llvm::StringRef line(line_info->buffer, line_info->lastchar - line_info->buffer); - if (std::optional<std::string> to_add = m_suggestion_callback(line)) { + std::string description; + if (std::optional<std::string> to_add = + m_suggestion_callback(line, description)) { LockedStreamFile locked_stream = m_output_stream_sp->Lock(); + // The suggestion is what gets inserted when accepted; the description (if + // any) is only shown in parentheses after it and never inserted. Only show + // the description up to its first newline so it stays on a single line. + std::string suggestion = to_add.value(); + if (!description.empty()) + suggestion += + " (" + description.substr(0, description.find('\n')) + ")"; + + int editline_cursor_position = + (int)((line_info->cursor - line_info->buffer) + GetPromptWidth()); + int editline_cursor_row = editline_cursor_position / m_terminal_width; + int toColumn = + editline_cursor_position - (editline_cursor_row * m_terminal_width); + + // The suggestion is drawn starting at the cursor and must fit on the + // remaining columns of the current row. If it is wider it would wrap onto + // the next line and corrupt the redraw, since we reposition the cursor by + // column only. Truncate it (with an ellipsis) so it always fits. + if (m_terminal_width > toColumn) { + const size_t available_width = m_terminal_width - toColumn; + if (suggestion.length() > available_width) { + llvm::StringRef ellipsis = "..."; + if (available_width > ellipsis.size()) + suggestion = suggestion.substr(0, available_width - ellipsis.size()) + + ellipsis.str(); + else + suggestion = suggestion.substr(0, available_width); + } + } + std::string to_add_color = - m_suggestion_ansi_prefix + to_add.value() + m_suggestion_ansi_suffix; + m_suggestion_ansi_prefix + suggestion + m_suggestion_ansi_suffix; fputs(typed.c_str(), locked_stream.GetFile().GetStream()); fputs(to_add_color.c_str(), locked_stream.GetFile().GetStream()); - size_t new_autosuggestion_size = line.size() + to_add->length(); + size_t new_autosuggestion_size = line.size() + suggestion.length(); // Print spaces to hide any remains of a previous longer autosuggestion. if (new_autosuggestion_size < m_previous_autosuggestion_size) { size_t spaces_to_print = @@ -1234,11 +1277,6 @@ unsigned char Editline::TypedCharacter(int ch) { } m_previous_autosuggestion_size = new_autosuggestion_size; - int editline_cursor_position = - (int)((line_info->cursor - line_info->buffer) + GetPromptWidth()); - int editline_cursor_row = editline_cursor_position / m_terminal_width; - int toColumn = - editline_cursor_position - (editline_cursor_row * m_terminal_width); fprintf(locked_stream.GetFile().GetStream(), ANSI_SET_COLUMN_N, toColumn); return CC_REFRESH; } diff --git a/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py b/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py index 1a6eef29a7f99..7eb347c8bb1ab 100644 --- a/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py +++ b/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py @@ -17,6 +17,7 @@ class TestCase(PExpectTest): ANSI_RESET = "\x1b[0m" ANSI_RED = "\x1b[31m" ANSI_CYAN = "\x1b[36m" + ANSI_CLEAR_RIGHT = "\x1b[K" # PExpect uses many timeouts internally and doesn't play well # under ASAN on a loaded machine.. @@ -161,6 +162,10 @@ def test_autosuggestion_tab_mode(self): tab completion would insert, instead of consulting command history.""" self.launch( use_colors=True, + # Use a wide terminal so the full description fits on one line and + # is not truncated (see test_autosuggestion_tab_mode_description_ + # truncation for the truncation behavior). + dimensions=(100, 500), extra_args=[ "-o", "settings set show-autosuggestion tab-mode", @@ -180,25 +185,74 @@ def test_autosuggestion_tab_mode(self): self.child.send("hel") # Note that the ANSI_RESET color prevents us from accepting a longer - # suggestion as a valid test outcome. + # suggestion as a valid test outcome. Because 'hel' has a single + # completion ('help'), tab-mode also shows that command's description + # in parentheses after the suggested 'p'. + help_description = ( + "Show a list of all debugger commands, " + "or give details about a specific command." + ) self.child.expect_exact( cursor_horizontal_abs("(lldb) he") + "l" + self.ANSI_FAINT - + "p" + + "p (" + help_description + ")" + self.ANSI_RESET ) # Applying the suggestion with Ctrl-F should leave the line as 'help' - # (not 'help frame'). Running it lists all commands. + # (not 'help frame', and without the parenthesized description). Running + # it lists all commands. self.child.send(ctrl_f + "\n") self.child.expect_exact("Debugger commands:") # Tab completion must still work in tab-mode. Pressing tab on 'hel' - # should complete to 'help'; running it lists all commands. - self.child.send("hel\t\n") + # should complete to 'help'; running it lists all commands. Accepting a + # completion must also clear the previously shown suggestion and its + # description from the line (rather than leaving the description behind). + self.child.send("hel") + self.child.expect_exact( + self.ANSI_FAINT + "p (" + help_description + ")" + self.ANSI_RESET + ) + self.child.send("\t") + # The whole line is cleared (\r + erase-to-end-of-line) and redrawn as + # 'help ', so no part of the description is left on screen. + self.child.expect_exact( + "\r" + self.ANSI_CLEAR_RIGHT + "(lldb) help " + ) + self.child.send("\n") self.child.expect_exact("Debugger commands:") + @skipIfAsan + @skipIfEditlineSupportMissing + def test_autosuggestion_tab_mode_description_truncation(self): + """Test that a tab-mode suggestion (including its description) that is + wider than the remaining space on the line is truncated with an + ellipsis, so it does not wrap and corrupt the terminal.""" + # Use a narrow terminal so the 'help' description does not fit. + self.launch( + use_colors=True, + dimensions=(24, 40), + extra_args=[ + "-o", + "settings set show-autosuggestion tab-mode", + "-o", + "settings set use-color true", + ], + ) + + # The suggestion for 'hel' starts at column 10 ('(lldb) hel'), leaving + # 30 columns on the 40-wide terminal. The suggestion is truncated to + # those 30 columns, the last three being the ellipsis. + self.child.send("hel") + self.child.expect_exact( + cursor_horizontal_abs("(lldb) he") + + "l" + + self.ANSI_FAINT + + "p (Show a list of all debug..." + + self.ANSI_RESET + ) + @skipIfAsan @skipIfEditlineSupportMissing def test_autosuggestion_tab_mode_no_common_prefix(self): _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
