diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h index 7bea0dbae082..252380de2786 100644 --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -273,6 +273,8 @@ public: bool SetUseColor(bool use_color); + bool GetUseAutosuggestion() const; + bool GetUseSourceCache() const; bool SetUseSourceCache(bool use_source_cache); diff --git a/lldb/include/lldb/Core/IOHandler.h b/lldb/include/lldb/Core/IOHandler.h index 51592afbbabe..f29482c0c97a 100644 --- a/lldb/include/lldb/Core/IOHandler.h +++ b/lldb/include/lldb/Core/IOHandler.h @@ -203,6 +203,9 @@ public: virtual void IOHandlerDeactivated(IOHandler &io_handler) {} + virtual llvm::Optional IOHandlerSuggestion(IOHandler &io_handler, + llvm::StringRef line); + virtual void IOHandlerComplete(IOHandler &io_handler, CompletionRequest &request); @@ -420,6 +423,9 @@ private: static int FixIndentationCallback(Editline *editline, const StringList &lines, int cursor_position, void *baton); + static llvm::Optional SuggestionCallback(llvm::StringRef line, + void *baton); + static void AutoCompleteCallback(CompletionRequest &request, void *baton); #endif diff --git a/lldb/include/lldb/Host/Editline.h b/lldb/include/lldb/Host/Editline.h index 356e8f734732..a37ad1b9d106 100644 --- a/lldb/include/lldb/Host/Editline.h +++ b/lldb/include/lldb/Host/Editline.h @@ -98,6 +98,9 @@ typedef int (*FixIndentationCallbackType)(Editline *editline, const StringList &lines, int cursor_position, void *baton); +typedef llvm::Optional (*SuggestionCallbackType)( + llvm::StringRef line, void *baton); + typedef void (*CompleteCallbackType)(CompletionRequest &request, void *baton); /// Status used to decide when and how to start editing another line in @@ -184,6 +187,9 @@ public: /// Cancel this edit and oblitarate all trace of it bool Cancel(); + /// Register a callback for autosuggestion. + void SetSuggestionCallback(SuggestionCallbackType callback, void *baton); + /// Register a callback for the tab key void SetAutoCompleteCallback(CompleteCallbackType callback, void *baton); @@ -312,6 +318,12 @@ private: /// tab key is typed. unsigned char TabCommand(int ch); + /// Apply autosuggestion part in gray as editline. + unsigned char ApplyAutosuggestCommand(int ch); + + /// Command used when a character is typed. + unsigned char TypedCharacter(int ch); + /// Respond to normal character insertion by fixing line indentation unsigned char FixIndentationCommand(int ch); @@ -360,7 +372,9 @@ private: const char *m_fix_indentation_callback_chars = nullptr; CompleteCallbackType m_completion_callback = nullptr; void *m_completion_callback_baton = nullptr; - + SuggestionCallbackType m_suggestion_callback = nullptr; + void *m_suggestion_callback_baton = nullptr; + std::size_t m_previous_autosuggestion_size = 0; std::mutex m_output_mutex; }; } diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h index 6ef22c1a28c1..36d7e5d3c118 100644 --- a/lldb/include/lldb/Interpreter/CommandInterpreter.h +++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h @@ -351,6 +351,10 @@ public: CommandObject *GetCommandObjectForCommand(llvm::StringRef &command_line); + /// Returns the auto-suggestion string that should be added to the given + /// command line. + llvm::Optional GetAutoSuggestionForCommand(llvm::StringRef line); + // This handles command line completion. void HandleCompletion(CompletionRequest &request); diff --git a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py index 86216ec034cd..67de73bf8970 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbpexpect.py +++ b/lldb/packages/Python/lldbsuite/test/lldbpexpect.py @@ -68,3 +68,10 @@ else: self.child.sendeof() self.child.close(force=not gracefully) self.child = None + + def cursor_forward_escape_seq(self, chars_to_move): + """ + Returns the escape sequence to move the cursor forward/right + by a certain amount of characters. + """ + return b"\x1b\[" + str(chars_to_move).encode("utf-8") + b"C" diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td index b04738175f34..96f67801553b 100644 --- a/lldb/source/Core/CoreProperties.td +++ b/lldb/source/Core/CoreProperties.td @@ -131,4 +131,8 @@ let Definition = "debugger" in { Global, DefaultStringValue<"frame #${frame.index}: ${ansi.fg.yellow}${frame.pc}${ansi.normal}{ ${module.file.basename}{`${function.name-without-args}{${frame.no-debug}${function.pc-offset}}}}{ at ${ansi.fg.cyan}${line.file.basename}${ansi.normal}:${ansi.fg.yellow}${line.number}${ansi.normal}{:${ansi.fg.yellow}${line.column}${ansi.normal}}}{${function.is-optimized} [opt]}{${frame.is-artificial} [artificial]}\\\\n">, Desc<"The default frame format string to use when displaying stack frameinformation for threads from thread backtrace unique.">; + def ShowAutosuggestion: Property<"show-autosuggestion", "Boolean">, + Global, + DefaultFalse, + Desc<"If true, LLDB will show suggestions to complete the command the user typed.">; } diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp index 1fb7393eef0a..61d77d03f893 100644 --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -346,6 +346,12 @@ bool Debugger::SetUseColor(bool b) { return ret; } +bool Debugger::GetUseAutosuggestion() const { + const uint32_t idx = ePropertyShowAutosuggestion; + return m_collection_sp->GetPropertyAtIndexAsBoolean( + nullptr, idx, g_debugger_properties[idx].default_uint_value != 0); +} + bool Debugger::GetUseSourceCache() const { const uint32_t idx = ePropertyUseSourceCache; return m_collection_sp->GetPropertyAtIndexAsBoolean( diff --git a/lldb/source/Core/IOHandler.cpp b/lldb/source/Core/IOHandler.cpp index 6cf09aaa7f9d..ca35d9fb315d 100644 --- a/lldb/source/Core/IOHandler.cpp +++ b/lldb/source/Core/IOHandler.cpp @@ -195,6 +195,14 @@ void IOHandlerConfirm::IOHandlerInputComplete(IOHandler &io_handler, } } +llvm::Optional +IOHandlerDelegate::IOHandlerSuggestion(IOHandler &io_handler, + llvm::StringRef line) { + return io_handler.GetDebugger() + .GetCommandInterpreter() + .GetAutoSuggestionForCommand(line); +} + void IOHandlerDelegate::IOHandlerComplete(IOHandler &io_handler, CompletionRequest &request) { switch (m_completion) { @@ -258,6 +266,8 @@ IOHandlerEditline::IOHandlerEditline( m_color_prompts); m_editline_up->SetIsInputCompleteCallback(IsInputCompleteCallback, this); m_editline_up->SetAutoCompleteCallback(AutoCompleteCallback, this); + if (debugger.GetUseAutosuggestion() && debugger.GetUseColor()) + m_editline_up->SetSuggestionCallback(SuggestionCallback, this); // See if the delegate supports fixing indentation const char *indent_chars = delegate.IOHandlerGetFixIndentationCharacters(); if (indent_chars) { @@ -430,6 +440,16 @@ int IOHandlerEditline::FixIndentationCallback(Editline *editline, *editline_reader, lines, cursor_position); } +llvm::Optional +IOHandlerEditline::SuggestionCallback(llvm::StringRef line, void *baton) { + IOHandlerEditline *editline_reader = static_cast(baton); + if (editline_reader) + return editline_reader->m_delegate.IOHandlerSuggestion(*editline_reader, + line); + + return llvm::None; +} + void IOHandlerEditline::AutoCompleteCallback(CompletionRequest &request, void *baton) { IOHandlerEditline *editline_reader = (IOHandlerEditline *)baton; diff --git a/lldb/source/Host/common/Editline.cpp b/lldb/source/Host/common/Editline.cpp index 1e1bcb0ac7c6..fc535d79b978 100644 --- a/lldb/source/Host/common/Editline.cpp +++ b/lldb/source/Host/common/Editline.cpp @@ -1009,7 +1009,10 @@ unsigned char Editline::TabCommand(int ch) { to_add.push_back(request.GetParsedArg().GetQuoteChar()); to_add.push_back(' '); el_insertstr(m_editline, to_add.c_str()); - break; + // Clear all the autosuggestion parts if the only single space can be completed. + if (to_add == " ") + return CC_REDISPLAY; + return CC_REFRESH; } case CompletionMode::Partial: { std::string to_add = completion.GetCompletion(); @@ -1043,6 +1046,52 @@ unsigned char Editline::TabCommand(int ch) { return CC_REDISPLAY; } +unsigned char Editline::ApplyAutosuggestCommand(int ch) { + const LineInfo *line_info = el_line(m_editline); + llvm::StringRef line(line_info->buffer, + line_info->lastchar - line_info->buffer); + + if (llvm::Optional to_add = + m_suggestion_callback(line, m_suggestion_callback_baton)) + el_insertstr(m_editline, to_add->c_str()); + + return CC_REDISPLAY; +} + +unsigned char Editline::TypedCharacter(int ch) { + std::string typed = std::string(1, ch); + el_insertstr(m_editline, typed.c_str()); + const LineInfo *line_info = el_line(m_editline); + llvm::StringRef line(line_info->buffer, + line_info->lastchar - line_info->buffer); + + if (llvm::Optional to_add = + m_suggestion_callback(line, m_suggestion_callback_baton)) { + std::string to_add_color = ANSI_FAINT + to_add.getValue() + ANSI_UNFAINT; + fputs(typed.c_str(), m_output_file); + fputs(to_add_color.c_str(), m_output_file); + size_t new_autosuggestion_size = line.size() + to_add->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 = + m_previous_autosuggestion_size - new_autosuggestion_size; + std::string spaces = std::string(spaces_to_print, ' '); + fputs(spaces.c_str(), m_output_file); + } + 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(m_output_file, ANSI_SET_COLUMN_N, toColumn); + return CC_REFRESH; + } + + return CC_REDISPLAY; +} + void Editline::ConfigureEditor(bool multiline) { if (m_editline && m_multiline_enabled == multiline) return; @@ -1156,7 +1205,38 @@ void Editline::ConfigureEditor(bool multiline) { if (!multiline) { el_set(m_editline, EL_BIND, "^r", "em-inc-search-prev", NULL); // Cycle through backwards search, entering string + + if (m_suggestion_callback) { + el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-apply-complete"), + EditLineConstString("Adopt autocompletion"), + (EditlineCommandCallbackType)([](EditLine *editline, int ch) { + return Editline::InstanceFor(editline)->ApplyAutosuggestCommand( + ch); + })); + + el_set(m_editline, EL_BIND, "^f", "lldb-apply-complete", + NULL); // Apply a part that is suggested automatically + + el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-typed-character"), + EditLineConstString("Typed character"), + (EditlineCommandCallbackType)([](EditLine *editline, int ch) { + return Editline::InstanceFor(editline)->TypedCharacter(ch); + })); + + char bind_key[2] = {0, 0}; + llvm::StringRef ascii_chars = + "abcdefghijklmnopqrstuvwxzyABCDEFGHIJKLMNOPQRSTUVWXZY1234567890!\"#$%" + "&'()*+,./:;<=>?@[]_`{|}~ "; + for (char c : ascii_chars) { + bind_key[0] = c; + el_set(m_editline, EL_BIND, bind_key, "lldb-typed-character", NULL); + } + el_set(m_editline, EL_BIND, "\\-", "lldb-typed-character", NULL); + el_set(m_editline, EL_BIND, "\\^", "lldb-typed-character", NULL); + el_set(m_editline, EL_BIND, "\\\\", "lldb-typed-character", NULL); + } } + el_set(m_editline, EL_BIND, "^w", "ed-delete-prev-word", NULL); // Delete previous word, behave like bash in emacs mode el_set(m_editline, EL_BIND, "\t", "lldb-complete", @@ -1367,6 +1447,12 @@ bool Editline::Cancel() { return result; } +void Editline::SetSuggestionCallback(SuggestionCallbackType callback, + void *baton) { + m_suggestion_callback = callback; + m_suggestion_callback_baton = baton; +} + void Editline::SetAutoCompleteCallback(CompleteCallbackType callback, void *baton) { m_completion_callback = callback; diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 4786e4602e4b..ec82efbe926d 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -1878,6 +1878,19 @@ void CommandInterpreter::HandleCompletion(CompletionRequest &request) { HandleCompletionMatches(request); } +llvm::Optional +CommandInterpreter::GetAutoSuggestionForCommand(llvm::StringRef line) { + if (line.empty()) + return llvm::None; + const size_t s = m_command_history.GetSize(); + for (int i = s - 1; i >= 0; --i) { + llvm::StringRef entry = m_command_history.GetStringAtIndex(i); + if (entry.consume_front(line)) + return entry.str(); + } + return llvm::None; +} + CommandInterpreter::~CommandInterpreter() {} void CommandInterpreter::UpdatePrompt(llvm::StringRef new_prompt) { diff --git a/lldb/test/API/commands/expression/multiline-completion/TestMultilineCompletion.py b/lldb/test/API/commands/expression/multiline-completion/TestMultilineCompletion.py index 3e2663d9bdfe..512b4a433984 100644 --- a/lldb/test/API/commands/expression/multiline-completion/TestMultilineCompletion.py +++ b/lldb/test/API/commands/expression/multiline-completion/TestMultilineCompletion.py @@ -40,7 +40,9 @@ class MultilineCompletionTest(PExpectTest): self.start_expression_editor() self.child.send("to_\t") - self.child.expect_exact("to_complete") + # editline might move the cursor back to the start of the line via \r + # and then back to its original position. + self.child.expect(re.compile(b"to_(\r" + self.cursor_forward_escape_seq(len(" 1: to_")) + b")?complete")) self.exit_expression_editor() # Check that completion empty input in a function with only one diff --git a/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py b/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py new file mode 100644 index 000000000000..3c8d20b2d1de --- /dev/null +++ b/lldb/test/API/iohandler/autosuggestion/TestAutosuggestion.py @@ -0,0 +1,105 @@ +""" +Tests autosuggestion using pexpect. +""" + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test.lldbpexpect import PExpectTest + +def cursor_horizontal_abs(s): + return "\x1b[" + str(len(s) + 1) + "G" + + + +class TestCase(PExpectTest): + + mydir = TestBase.compute_mydir(__file__) + + # PExpect uses many timeouts internally and doesn't play well + # under ASAN on a loaded machine.. + @skipIfAsan + @skipIfEditlineSupportMissing + def test_autosuggestion_add_spaces(self): + self.launch(extra_args=["-o", "settings set show-autosuggestion true", "-o", "settings set use-color true"]) + + # Common input codes and escape sequences. + faint_color = "\x1b[2m" + reset = "\x1b[0m" + + # Check if spaces are added to hide the previous gray characters. + self.expect("help frame var") + self.expect("help frame info") + self.child.send("help frame v") + self.child.expect_exact(cursor_horizontal_abs("(lldb) help frame ") + "v" + faint_color + "ar" + reset + " ") + + self.quit() + + @skipIfAsan + @skipIfEditlineSupportMissing + def test_autosuggestion(self): + self.launch(extra_args=["-o", "settings set show-autosuggestion true", "-o", "settings set use-color true"]) + + # Common input codes and escape sequences. + ctrl_f = "\x06" + faint_color = "\x1b[2m" + reset = "\x1b[0m" + delete = chr(127) + + frame_output_needle = "Syntax: frame " + # Run 'help frame' once to put it into the command history. + self.expect("help frame", substrs=[frame_output_needle]) + + # Check that LLDB shows the autosuggestion in gray behind the text. + self.child.send("hel") + self.child.expect_exact(cursor_horizontal_abs("(lldb) he") + "l" + faint_color + "p frame" + reset) + + # Apply the autosuggestion and press enter. This should print the + # 'help frame' output if everything went correctly. + self.child.send(ctrl_f + "\n") + self.child.expect_exact(frame_output_needle) + + # Check that pressing Ctrl+F directly after Ctrl+F again does nothing. + self.child.send("hel" + ctrl_f + ctrl_f + "\n") + self.child.expect_exact(frame_output_needle) + + # Try autosuggestion using tab and ^f. + # \t makes "help" and ^f makes "help frame". If everything went + # correct we should see the 'help frame' output again. + self.child.send("hel\t" + ctrl_f + "\n") + self.child.expect_exact(frame_output_needle) + + # Check that autosuggestion works after delete. + self.child.send("a1234" + 5 * delete + "hel" + ctrl_f + "\n") + self.child.expect_exact(frame_output_needle) + + # Check that autosuggestion works after delete. + self.child.send("help x" + delete + ctrl_f + "\n") + self.child.expect_exact(frame_output_needle) + + # Check that autosuggestion complete to the most recent one. + self.child.send("help frame variable\n") + self.child.send("help fr") + self.child.expect_exact(faint_color + "ame variable" + reset) + self.child.send("\n") + + # Try another command. + apropos_output_needle = "Syntax: apropos " + # Run 'help frame' once to put it into the command history. + self.expect("help apropos", substrs=[apropos_output_needle]) + + # Check that 'hel' should have an autosuggestion for 'help apropos' now. + self.child.send("hel") + self.child.expect_exact(cursor_horizontal_abs("(lldb) he") + "l" + faint_color + "p apropos" + reset) + + # Run the command and expect the 'help apropos' output. + self.child.send(ctrl_f + "\n") + self.child.expect_exact(apropos_output_needle) + + # Check that pressing Ctrl+F in an empty prompt does nothing. + breakpoint_output_needle = "Syntax: breakpoint " + self.child.send(ctrl_f + "help breakpoint" +"\n") + self.child.expect_exact(breakpoint_output_needle) + + + self.quit() diff --git a/lldb/test/API/iohandler/completion/TestIOHandlerCompletion.py b/lldb/test/API/iohandler/completion/TestIOHandlerCompletion.py index 610bf019436a..f5904084470b 100644 --- a/lldb/test/API/iohandler/completion/TestIOHandlerCompletion.py +++ b/lldb/test/API/iohandler/completion/TestIOHandlerCompletion.py @@ -26,7 +26,9 @@ class IOHandlerCompletionTest(PExpectTest): # Try tab completing regi to register. self.child.send("regi\t") - self.child.expect_exact(self.PROMPT + "register") + # editline might move the cursor back to the start of the line and + # then back to its original position. + self.child.expect(re.compile(b"regi(\r" + self.cursor_forward_escape_seq(len(self.PROMPT + "regi")) + b")?ster")) self.child.send("\n") self.expect_prompt() @@ -39,7 +41,11 @@ class IOHandlerCompletionTest(PExpectTest): # If we get a correct partial completion without a trailing space, then this # should complete the current test file. self.child.send("TestIOHandler\t") - self.child.expect_exact("TestIOHandlerCompletion.py") + # As above, editline might move the cursor to the start of the line and + # then back to its original position. We only care about the fact + # that this is completing a partial completion, so skip the exact cursor + # position calculation. + self.child.expect(re.compile(b"TestIOHandler(\r" + self.cursor_forward_escape_seq("\d+") + b")?Completion.py")) self.child.send("\n") self.expect_prompt()