//===-- Editline.cpp --------------------------------------------*- C++ -*-===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// #include #include #include #include "lldb/Host/Editline.h" #include "lldb/Host/ConnectionFileDescriptor.h" #include "lldb/Core/Error.h" #include "lldb/Core/StringList.h" #include "lldb/Core/StreamString.h" #include "lldb/Host/FileSpec.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/Host.h" #include "lldb/Utility/LLDBAssert.h" using namespace lldb_private; using namespace lldb_private::line_editor; // Workaround for what looks like an OS X-specific issue, but other platforms // may benefit from something similar if issues arise. The libedit library // doesn't explicitly initialize the curses termcap library, which it gets away // with until TERM is set to VT100 where it stumbles over an implementation // assumption that may not exist on other platforms. The setupterm() function // would normally require headers that don't work gracefully in this context, so // the function declaraction has been hoisted here. #if defined(__APPLE__) extern "C" { int setupterm(char *term, int fildes, int *errret); } #define USE_SETUPTERM_WORKAROUND #endif // Editline uses careful cursor management to achieve the illusion of editing a multi-line block of text // with a single line editor. Preserving this illusion requires fairly careful management of cursor // state. Read and understand the relationship between DisplayInput(), MoveCursor(), SetCurrentLine(), // and SaveEditedLine() before making changes. #define ESCAPE "\x1b" #define ANSI_FAINT ESCAPE "[2m" #define ANSI_UNFAINT ESCAPE "[22m" #define ANSI_CLEAR_BELOW ESCAPE "[J" #define ANSI_CLEAR_RIGHT ESCAPE "[K" #define ANSI_SET_COLUMN_N ESCAPE "[%dG" #define ANSI_UP_N_ROWS ESCAPE "[%dA" #define ANSI_DOWN_N_ROWS ESCAPE "[%dB" #if LLDB_EDITLINE_USE_WCHAR #define EditLineConstString(str) L##str #define EditLineStringFormatSpec "%ls" #else #define EditLineConstString(str) str #define EditLineStringFormatSpec "%s" // use #defines so wide version functions and structs will resolve to old versions // for case of libedit not built with wide char support #define history_w history #define history_winit history_init #define history_wend history_end #define HistoryW History #define HistEventW HistEvent #define LineInfoW LineInfo #define el_wgets el_gets #define el_wgetc el_getc #define el_wpush el_push #define el_wparse el_parse #define el_wset el_set #define el_wget el_get #define el_wline el_line #define el_winsertstr el_insertstr #define el_wdeletestr el_deletestr #endif // #if LLDB_EDITLINE_USE_WCHAR bool IsOnlySpaces (const EditLineStringType & content) { for (wchar_t ch : content) { if (ch != EditLineCharType(' ')) return false; } return true; } EditLineStringType CombineLines (const std::vector & lines) { EditLineStringStreamType combined_stream; for (EditLineStringType line : lines) { combined_stream << line.c_str() << "\n"; } return combined_stream.str(); } std::vector SplitLines (const EditLineStringType & input) { std::vector result; size_t start = 0; while (start < input.length()) { size_t end = input.find ('\n', start); if (end == std::string::npos) { result.insert (result.end(), input.substr (start)); break; } result.insert (result.end(), input.substr (start, end - start)); start = end + 1; } return result; } EditLineStringType FixIndentation (const EditLineStringType & line, int indent_correction) { if (indent_correction == 0) return line; if (indent_correction < 0) return line.substr (-indent_correction); return EditLineStringType (indent_correction, EditLineCharType(' ')) + line; } int GetIndentation (const EditLineStringType & line) { int space_count = 0; for (EditLineCharType ch : line) { if (ch != EditLineCharType(' ')) break; ++space_count; } return space_count; } bool IsInputPending (FILE * file) { // FIXME: This will be broken on Windows if we ever re-enable Editline. You can't use select // on something that isn't a socket. This will have to be re-written to not use a FILE*, but // instead use some kind of yet-to-be-created abstraction that select-like functionality on // non-socket objects. const int fd = fileno (file); fd_set fds; FD_ZERO (&fds); FD_SET (fd, &fds); timeval timeout = { 0, 0 }; return select (fd + 1, &fds, NULL, NULL, &timeout); } namespace lldb_private { namespace line_editor { typedef std::weak_ptr EditlineHistoryWP; // EditlineHistory objects are sometimes shared between multiple // Editline instances with the same program name. class EditlineHistory { private: // Use static GetHistory() function to get a EditlineHistorySP to one of these objects EditlineHistory (const std::string &prefix, uint32_t size, bool unique_entries) : m_history (NULL), m_event (), m_prefix (prefix), m_path () { m_history = history_winit(); history_w (m_history, &m_event, H_SETSIZE, size); if (unique_entries) history_w (m_history, &m_event, H_SETUNIQUE, 1); } const char * GetHistoryFilePath() { if (m_path.empty() && m_history && !m_prefix.empty()) { FileSpec parent_path{"~/.lldb", true}; char history_path[PATH_MAX]; if (FileSystem::MakeDirectory(parent_path, lldb::eFilePermissionsDirectoryDefault).Success()) { snprintf (history_path, sizeof (history_path), "~/.lldb/%s-history", m_prefix.c_str()); } else { snprintf (history_path, sizeof (history_path), "~/%s-widehistory", m_prefix.c_str()); } m_path = FileSpec (history_path, true).GetPath(); } if (m_path.empty()) return NULL; return m_path.c_str(); } public: ~EditlineHistory() { Save(); if (m_history) { history_wend (m_history); m_history = NULL; } } static EditlineHistorySP GetHistory (const std::string &prefix) { typedef std::map WeakHistoryMap; static std::recursive_mutex g_mutex; static WeakHistoryMap g_weak_map; std::lock_guard guard(g_mutex); WeakHistoryMap::const_iterator pos = g_weak_map.find (prefix); EditlineHistorySP history_sp; if (pos != g_weak_map.end()) { history_sp = pos->second.lock(); if (history_sp) return history_sp; g_weak_map.erase (pos); } history_sp.reset (new EditlineHistory (prefix, 800, true)); g_weak_map[prefix] = history_sp; return history_sp; } bool IsValid() const { return m_history != NULL; } HistoryW * GetHistoryPtr () { return m_history; } void Enter (const EditLineCharType *line_cstr) { if (m_history) history_w (m_history, &m_event, H_ENTER, line_cstr); } bool Load () { if (m_history) { const char *path = GetHistoryFilePath(); if (path) { history_w (m_history, &m_event, H_LOAD, path); return true; } } return false; } bool Save () { if (m_history) { const char *path = GetHistoryFilePath(); if (path) { history_w (m_history, &m_event, H_SAVE, path); return true; } } return false; } protected: HistoryW * m_history; // The history object HistEventW m_event; // The history event needed to contain all history events std::string m_prefix; // The prefix name (usually the editline program name) to use when loading/saving history std::string m_path; // Path to the history file }; } } //------------------------------------------------------------------ // Editline private methods //------------------------------------------------------------------ void Editline::SetBaseLineNumber (int line_number) { std::stringstream line_number_stream; line_number_stream << line_number; m_base_line_number = line_number; m_line_number_digits = std::max (3, (int)line_number_stream.str().length() + 1); } std::string Editline::PromptForIndex (int line_index) { bool use_line_numbers = m_multiline_enabled && m_base_line_number > 0; std::string prompt = m_set_prompt; if (use_line_numbers && prompt.length() == 0) { prompt = ": "; } std::string continuation_prompt = prompt; if (m_set_continuation_prompt.length() > 0) { continuation_prompt = m_set_continuation_prompt; // Ensure that both prompts are the same length through space padding while (continuation_prompt.length() < prompt.length()) { continuation_prompt += ' '; } while (prompt.length() < continuation_prompt.length()) { prompt += ' '; } } if (use_line_numbers) { StreamString prompt_stream; prompt_stream.Printf("%*d%s", m_line_number_digits, m_base_line_number + line_index, (line_index == 0) ? prompt.c_str() : continuation_prompt.c_str()); return std::move (prompt_stream.GetString()); } return (line_index == 0) ? prompt : continuation_prompt; } void Editline::SetCurrentLine (int line_index) { m_current_line_index = line_index; m_current_prompt = PromptForIndex (line_index); } int Editline::GetPromptWidth() { return (int)PromptForIndex (0).length(); } bool Editline::IsEmacs() { const char * editor; el_get (m_editline, EL_EDITOR, &editor); return editor[0] == 'e'; } bool Editline::IsOnlySpaces() { const LineInfoW * info = el_wline (m_editline); for (const EditLineCharType * character = info->buffer; character < info->lastchar; character++) { if (*character != ' ') return false; } return true; } int Editline::GetLineIndexForLocation (CursorLocation location, int cursor_row) { int line = 0; if (location == CursorLocation::EditingPrompt || location == CursorLocation::BlockEnd || location == CursorLocation::EditingCursor) { for (unsigned index = 0; index < m_current_line_index; index++) { line += CountRowsForLine (m_input_lines[index]); } if (location == CursorLocation::EditingCursor) { line += cursor_row; } else if (location == CursorLocation::BlockEnd) { for (unsigned index = m_current_line_index; index < m_input_lines.size(); index++) { line += CountRowsForLine (m_input_lines[index]); } --line; } } return line; } void Editline::MoveCursor (CursorLocation from, CursorLocation to) { const LineInfoW * info = el_wline (m_editline); int editline_cursor_position = (int)((info->cursor - info->buffer) + GetPromptWidth()); int editline_cursor_row = editline_cursor_position / m_terminal_width; // Determine relative starting and ending lines int fromLine = GetLineIndexForLocation (from, editline_cursor_row); int toLine = GetLineIndexForLocation (to, editline_cursor_row); if (toLine != fromLine) { fprintf (m_output_file, (toLine > fromLine) ? ANSI_DOWN_N_ROWS : ANSI_UP_N_ROWS, std::abs (toLine - fromLine)); } // Determine target column int toColumn = 1; if (to == CursorLocation::EditingCursor) { toColumn = editline_cursor_position - (editline_cursor_row * m_terminal_width) + 1; } else if (to == CursorLocation::BlockEnd) { toColumn = ((m_input_lines[m_input_lines.size() - 1].length() + GetPromptWidth()) % 80) + 1; } fprintf (m_output_file, ANSI_SET_COLUMN_N, toColumn); } void Editline::DisplayInput (int firstIndex) { fprintf (m_output_file, ANSI_SET_COLUMN_N ANSI_CLEAR_BELOW, 1); int line_count = (int)m_input_lines.size(); const char *faint = m_color_prompts ? ANSI_FAINT : ""; const char *unfaint = m_color_prompts ? ANSI_UNFAINT : ""; for (int index = firstIndex; index < line_count; index++) { fprintf (m_output_file, "%s" "%s" "%s" EditLineStringFormatSpec " ", faint, PromptForIndex (index).c_str(), unfaint, m_input_lines[index].c_str()); if (index < line_count - 1) fprintf (m_output_file, "\n"); } } int Editline::CountRowsForLine (const EditLineStringType & content) { auto prompt = PromptForIndex (0); // Prompt width is constant during an edit session int line_length = (int)(content.length() + prompt.length()); return (line_length / m_terminal_width) + 1; } void Editline::SaveEditedLine() { const LineInfoW * info = el_wline (m_editline); m_input_lines[m_current_line_index] = EditLineStringType (info->buffer, info->lastchar - info->buffer); } StringList Editline::GetInputAsStringList(int line_count) { StringList lines; for (EditLineStringType line : m_input_lines) { if (line_count == 0) break; #if LLDB_EDITLINE_USE_WCHAR lines.AppendString (m_utf8conv.to_bytes (line)); #else lines.AppendString(line); #endif --line_count; } return lines; } unsigned char Editline::RecallHistory (bool earlier) { if (!m_history_sp || !m_history_sp->IsValid()) return CC_ERROR; HistoryW * pHistory = m_history_sp->GetHistoryPtr(); HistEventW history_event; std::vector new_input_lines; // Treat moving from the "live" entry differently if (!m_in_history) { if (earlier == false) return CC_ERROR; // Can't go newer than the "live" entry if (history_w (pHistory, &history_event, H_FIRST) == -1) return CC_ERROR; // Save any edits to the "live" entry in case we return by moving forward in history // (it would be more bash-like to save over any current entry, but libedit doesn't // offer the ability to add entries anywhere except the end.) SaveEditedLine(); m_live_history_lines = m_input_lines; m_in_history = true; } else { if (history_w (pHistory, &history_event, earlier ? H_NEXT : H_PREV) == -1) { // Can't move earlier than the earliest entry if (earlier) return CC_ERROR; // ... but moving to newer than the newest yields the "live" entry new_input_lines = m_live_history_lines; m_in_history = false; } } // If we're pulling the lines from history, split them apart if (m_in_history) new_input_lines = SplitLines (history_event.str); // Erase the current edit session and replace it with a new one MoveCursor (CursorLocation::EditingCursor, CursorLocation::BlockStart); m_input_lines = new_input_lines; DisplayInput(); // Prepare to edit the last line when moving to previous entry, or the first line // when moving to next entry SetCurrentLine (m_current_line_index = earlier ? (int)m_input_lines.size() - 1 : 0); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingPrompt); return CC_NEWLINE; } int Editline::GetCharacter (EditLineCharType * c) { const LineInfoW * info = el_wline (m_editline); // Paint a faint version of the desired prompt over the version libedit draws // (will only be requested if colors are supported) if (m_needs_prompt_repaint) { MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); fprintf (m_output_file, "%s" "%s" "%s", ANSI_FAINT, Prompt(), ANSI_UNFAINT); MoveCursor (CursorLocation::EditingPrompt, CursorLocation::EditingCursor); m_needs_prompt_repaint = false; } if (m_multiline_enabled) { // Detect when the number of rows used for this input line changes due to an edit int lineLength = (int)((info->lastchar - info->buffer) + GetPromptWidth()); int new_line_rows = (lineLength / m_terminal_width) + 1; if (m_current_line_rows != -1 && new_line_rows != m_current_line_rows) { // Respond by repainting the current state from this line on MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); SaveEditedLine(); DisplayInput (m_current_line_index); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingCursor); } m_current_line_rows = new_line_rows; } // Read an actual character while (true) { lldb::ConnectionStatus status = lldb::eConnectionStatusSuccess; char ch = 0; // This mutex is locked by our caller (GetLine). Unlock it while we read a character // (blocking operation), so we do not hold the mutex indefinitely. This gives a chance // for someone to interrupt us. After Read returns, immediately lock the mutex again and // check if we were interrupted. m_output_mutex.unlock(); int read_count = m_input_connection.Read(&ch, 1, UINT32_MAX, status, NULL); m_output_mutex.lock(); if (m_editor_status == EditorStatus::Interrupted) { while (read_count > 0 && status == lldb::eConnectionStatusSuccess) read_count = m_input_connection.Read(&ch, 1, UINT32_MAX, status, NULL); lldbassert(status == lldb::eConnectionStatusInterrupted); return 0; } if (read_count) { #if LLDB_EDITLINE_USE_WCHAR // After the initial interruptible read, this is guaranteed not to block ungetc (ch, m_input_file); *c = fgetwc (m_input_file); if (*c != WEOF) return 1; #else *c = ch; if(ch != (char)EOF) return 1; #endif } else { switch (status) { case lldb::eConnectionStatusSuccess: // Success break; case lldb::eConnectionStatusInterrupted: lldbassert(0 && "Interrupts should have been handled above."); case lldb::eConnectionStatusError: // Check GetError() for details case lldb::eConnectionStatusTimedOut: // Request timed out case lldb::eConnectionStatusEndOfFile: // End-of-file encountered case lldb::eConnectionStatusNoConnection: // No connection case lldb::eConnectionStatusLostConnection: // Lost connection while connected to a valid connection m_editor_status = EditorStatus::EndOfInput; return 0; } } } } const char * Editline::Prompt() { if (m_color_prompts) m_needs_prompt_repaint = true; return m_current_prompt.c_str(); } unsigned char Editline::BreakLineCommand (int ch) { // Preserve any content beyond the cursor, truncate and save the current line const LineInfoW * info = el_wline (m_editline); auto current_line = EditLineStringType (info->buffer, info->cursor - info->buffer); auto new_line_fragment = EditLineStringType (info->cursor, info->lastchar - info->cursor); m_input_lines[m_current_line_index] = current_line; // Ignore whitespace-only extra fragments when breaking a line if (::IsOnlySpaces (new_line_fragment)) new_line_fragment = EditLineConstString(""); // Establish the new cursor position at the start of a line when inserting a line break m_revert_cursor_index = 0; // Don't perform automatic formatting when pasting if (!IsInputPending (m_input_file)) { // Apply smart indentation if (m_fix_indentation_callback) { StringList lines = GetInputAsStringList (m_current_line_index + 1); #if LLDB_EDITLINE_USE_WCHAR lines.AppendString (m_utf8conv.to_bytes (new_line_fragment)); #else lines.AppendString (new_line_fragment); #endif int indent_correction = m_fix_indentation_callback (this, lines, 0, m_fix_indentation_callback_baton); new_line_fragment = FixIndentation(new_line_fragment, indent_correction); m_revert_cursor_index = GetIndentation(new_line_fragment); } } // Insert the new line and repaint everything from the split line on down m_input_lines.insert (m_input_lines.begin() + m_current_line_index + 1, new_line_fragment); MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); DisplayInput (m_current_line_index); // Reposition the cursor to the right line and prepare to edit the new line SetCurrentLine (m_current_line_index + 1); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingPrompt); return CC_NEWLINE; } unsigned char Editline::EndOrAddLineCommand(int ch) { // Don't perform end of input detection when pasting, always treat this as a line break if (IsInputPending(m_input_file)) { return BreakLineCommand(ch); } // Save any edits to this line SaveEditedLine(); // If this is the end of the last line, consider whether to add a line instead const LineInfoW *info = el_wline(m_editline); if (m_current_line_index == m_input_lines.size() - 1 && info->cursor == info->lastchar) { if (m_is_input_complete_callback) { auto lines = GetInputAsStringList(); if (!m_is_input_complete_callback(this, lines, m_is_input_complete_callback_baton)) { return BreakLineCommand(ch); } // The completion test is allowed to change the input lines when complete m_input_lines.clear(); for (unsigned index = 0; index < lines.GetSize(); index++) { #if LLDB_EDITLINE_USE_WCHAR m_input_lines.insert(m_input_lines.end(), m_utf8conv.from_bytes(lines[index])); #else m_input_lines.insert(m_input_lines.end(), lines[index]); #endif } } } MoveCursor(CursorLocation::EditingCursor, CursorLocation::BlockEnd); fprintf(m_output_file, "\n"); m_editor_status = EditorStatus::Complete; return CC_NEWLINE; } unsigned char Editline::DeleteNextCharCommand(int ch) { LineInfoW * info = const_cast(el_wline (m_editline)); // Just delete the next character normally if possible if (info->cursor < info->lastchar) { info->cursor++; el_deletestr (m_editline, 1); return CC_REFRESH; } // Fail when at the end of the last line, except when ^D is pressed on // the line is empty, in which case it is treated as EOF if (m_current_line_index == m_input_lines.size() - 1) { if (ch == 4 && info->buffer == info->lastchar) { fprintf (m_output_file, "^D\n"); m_editor_status = EditorStatus::EndOfInput; return CC_EOF; } return CC_ERROR; } // Prepare to combine this line with the one below MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); // Insert the next line of text at the cursor and restore the cursor position const EditLineCharType * cursor = info->cursor; el_winsertstr (m_editline, m_input_lines[m_current_line_index + 1].c_str()); info->cursor = cursor; SaveEditedLine(); // Delete the extra line m_input_lines.erase (m_input_lines.begin() + m_current_line_index + 1); // Clear and repaint from this line on down DisplayInput (m_current_line_index); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingCursor); return CC_REFRESH; } unsigned char Editline::DeletePreviousCharCommand (int ch) { LineInfoW * info = const_cast(el_wline (m_editline)); // Just delete the previous character normally when not at the start of a line if (info->cursor > info->buffer) { el_deletestr (m_editline, 1); return CC_REFRESH; } // No prior line and no prior character? Let the user know if (m_current_line_index == 0) return CC_ERROR; // No prior character, but prior line? Combine with the line above SaveEditedLine(); SetCurrentLine (m_current_line_index - 1); auto priorLine = m_input_lines[m_current_line_index]; m_input_lines.erase (m_input_lines.begin() + m_current_line_index); m_input_lines[m_current_line_index] = priorLine + m_input_lines[m_current_line_index]; // Repaint from the new line down fprintf (m_output_file, ANSI_UP_N_ROWS ANSI_SET_COLUMN_N, CountRowsForLine (priorLine), 1); DisplayInput (m_current_line_index); // Put the cursor back where libedit expects it to be before returning to editing // by telling libedit about the newly inserted text MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingPrompt); el_winsertstr (m_editline, priorLine.c_str()); return CC_REDISPLAY; } unsigned char Editline::PreviousLineCommand (int ch) { SaveEditedLine(); if (m_current_line_index == 0) { return RecallHistory (true); } // Start from a known location MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); // Treat moving up from a blank last line as a deletion of that line if (m_current_line_index == m_input_lines.size() - 1 && IsOnlySpaces()) { m_input_lines.erase (m_input_lines.begin() + m_current_line_index); fprintf (m_output_file, ANSI_CLEAR_BELOW); } SetCurrentLine (m_current_line_index - 1); fprintf (m_output_file, ANSI_UP_N_ROWS ANSI_SET_COLUMN_N, CountRowsForLine (m_input_lines[m_current_line_index]), 1); return CC_NEWLINE; } unsigned char Editline::NextLineCommand (int ch) { SaveEditedLine(); // Handle attempts to move down from the last line if (m_current_line_index == m_input_lines.size() - 1) { // Don't add an extra line if the existing last line is blank, move through history instead if (IsOnlySpaces()) { return RecallHistory (false); } // Determine indentation for the new line int indentation = 0; if (m_fix_indentation_callback) { StringList lines = GetInputAsStringList(); lines.AppendString(""); indentation = m_fix_indentation_callback (this, lines, 0, m_fix_indentation_callback_baton); } m_input_lines.insert (m_input_lines.end(), EditLineStringType (indentation, EditLineCharType(' '))); } // Move down past the current line using newlines to force scrolling if needed SetCurrentLine (m_current_line_index + 1); const LineInfoW * info = el_wline (m_editline); int cursor_position = (int)((info->cursor - info->buffer) + GetPromptWidth()); int cursor_row = cursor_position / m_terminal_width; for (int line_count = 0; line_count < m_current_line_rows - cursor_row; line_count++) { fprintf (m_output_file, "\n"); } return CC_NEWLINE; } unsigned char Editline::PreviousHistoryCommand(int ch) { SaveEditedLine(); return RecallHistory(true); } unsigned char Editline::NextHistoryCommand(int ch) { SaveEditedLine(); return RecallHistory(false); } unsigned char Editline::FixIndentationCommand(int ch) { if (!m_fix_indentation_callback) return CC_NORM; // Insert the character typed before proceeding EditLineCharType inserted[] = { (EditLineCharType)ch, 0 }; el_winsertstr (m_editline, inserted); LineInfoW * info = const_cast(el_wline (m_editline)); int cursor_position = info->cursor - info->buffer; // Save the edits and determine the correct indentation level SaveEditedLine(); StringList lines = GetInputAsStringList (m_current_line_index + 1); int indent_correction = m_fix_indentation_callback (this, lines, cursor_position, m_fix_indentation_callback_baton); // If it is already correct no special work is needed if (indent_correction == 0) return CC_REFRESH; // Change the indentation level of the line std::string currentLine = lines.GetStringAtIndex (m_current_line_index); if (indent_correction > 0) { currentLine = currentLine.insert (0, indent_correction, ' '); } else { currentLine = currentLine.erase (0, -indent_correction); } #if LLDB_EDITLINE_USE_WCHAR m_input_lines[m_current_line_index] = m_utf8conv.from_bytes (currentLine); #else m_input_lines[m_current_line_index] = currentLine; #endif // Update the display to reflect the change MoveCursor (CursorLocation::EditingCursor, CursorLocation::EditingPrompt); DisplayInput (m_current_line_index); // Reposition the cursor back on the original line and prepare to restart editing // with a new cursor position SetCurrentLine (m_current_line_index); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingPrompt); m_revert_cursor_index = cursor_position + indent_correction; return CC_NEWLINE; } unsigned char Editline::RevertLineCommand (int ch) { el_winsertstr (m_editline, m_input_lines[m_current_line_index].c_str()); if (m_revert_cursor_index >= 0) { LineInfoW * info = const_cast(el_wline (m_editline)); info->cursor = info->buffer + m_revert_cursor_index; if (info->cursor > info->lastchar) { info->cursor = info->lastchar; } m_revert_cursor_index = -1; } return CC_REFRESH; } unsigned char Editline::BufferStartCommand (int ch) { SaveEditedLine(); MoveCursor (CursorLocation::EditingCursor, CursorLocation::BlockStart); SetCurrentLine (0); m_revert_cursor_index = 0; return CC_NEWLINE; } unsigned char Editline::BufferEndCommand (int ch) { SaveEditedLine(); MoveCursor (CursorLocation::EditingCursor, CursorLocation::BlockEnd); SetCurrentLine ((int)m_input_lines.size() - 1); MoveCursor (CursorLocation::BlockEnd, CursorLocation::EditingPrompt); return CC_NEWLINE; } unsigned char Editline::TabCommand (int ch) { if (m_completion_callback == nullptr) return CC_ERROR; const LineInfo *line_info = el_line (m_editline); StringList completions; int page_size = 40; const int num_completions = m_completion_callback (line_info->buffer, line_info->cursor, line_info->lastchar, 0, // Don't skip any matches (start at match zero) -1, // Get all the matches completions, m_completion_callback_baton); if (num_completions == 0) return CC_ERROR; // if (num_completions == -1) // { // el_insertstr (m_editline, m_completion_key); // return CC_REDISPLAY; // } // else if (num_completions == -2) { // Replace the entire line with the first string... el_deletestr (m_editline, line_info->cursor - line_info->buffer); el_insertstr (m_editline, completions.GetStringAtIndex (0)); return CC_REDISPLAY; } // If we get a longer match display that first. const char *completion_str = completions.GetStringAtIndex (0); if (completion_str != nullptr && *completion_str != '\0') { el_insertstr (m_editline, completion_str); return CC_REDISPLAY; } if (num_completions > 1) { int num_elements = num_completions + 1; fprintf (m_output_file, "\n" ANSI_CLEAR_BELOW "Available completions:"); if (num_completions < page_size) { for (int i = 1; i < num_elements; i++) { completion_str = completions.GetStringAtIndex (i); fprintf (m_output_file, "\n\t%s", completion_str); } fprintf (m_output_file, "\n"); } else { int cur_pos = 1; char reply; int got_char; while (cur_pos < num_elements) { int endpoint = cur_pos + page_size; if (endpoint > num_elements) endpoint = num_elements; for (; cur_pos < endpoint; cur_pos++) { completion_str = completions.GetStringAtIndex (cur_pos); fprintf (m_output_file, "\n\t%s", completion_str); } if (cur_pos >= num_elements) { fprintf (m_output_file, "\n"); break; } fprintf (m_output_file, "\nMore (Y/n/a): "); reply = 'n'; got_char = el_getc(m_editline, &reply); if (got_char == -1 || reply == 'n') break; if (reply == 'a') page_size = num_elements - cur_pos; } } DisplayInput(); MoveCursor(CursorLocation::BlockEnd, CursorLocation::EditingCursor); } return CC_REDISPLAY; } void Editline::ConfigureEditor (bool multiline) { if (m_editline && m_multiline_enabled == multiline) return; m_multiline_enabled = multiline; if (m_editline) { // Disable edit mode to stop the terminal from flushing all input // during the call to el_end() since we expect to have multiple editline // instances in this program. el_set (m_editline, EL_EDITMODE, 0); el_end (m_editline); } m_editline = el_init (m_editor_name.c_str(), m_input_file, m_output_file, m_error_file); TerminalSizeChanged(); if (m_history_sp && m_history_sp->IsValid()) { m_history_sp->Load(); el_wset (m_editline, EL_HIST, history, m_history_sp->GetHistoryPtr()); } el_set (m_editline, EL_CLIENTDATA, this); el_set (m_editline, EL_SIGNAL, 0); el_set (m_editline, EL_EDITOR, "emacs"); el_set (m_editline, EL_PROMPT, (EditlinePromptCallbackType)([] (EditLine *editline) { return Editline::InstanceFor (editline)->Prompt(); })); el_wset (m_editline, EL_GETCFN, (EditlineGetCharCallbackType)([] (EditLine * editline, EditLineCharType * c) { return Editline::InstanceFor (editline)->GetCharacter (c); })); // Commands used for multiline support, registered whether or not they're used el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-break-line"), EditLineConstString("Insert a line break"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->BreakLineCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-end-or-add-line"), EditLineConstString("End editing or continue when incomplete"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->EndOrAddLineCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-delete-next-char"), EditLineConstString("Delete next character"), (EditlineCommandCallbackType)([](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->DeleteNextCharCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-delete-previous-char"), EditLineConstString("Delete previous character"), (EditlineCommandCallbackType)([](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->DeletePreviousCharCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-previous-line"), EditLineConstString("Move to previous line"), (EditlineCommandCallbackType)([](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->PreviousLineCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-next-line"), EditLineConstString("Move to next line"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->NextLineCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-previous-history"), EditLineConstString("Move to previous history"), (EditlineCommandCallbackType)([](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->PreviousHistoryCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-next-history"), EditLineConstString("Move to next history"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->NextHistoryCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-buffer-start"), EditLineConstString("Move to start of buffer"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->BufferStartCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-buffer-end"), EditLineConstString("Move to end of buffer"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->BufferEndCommand(ch); })); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-fix-indentation"), EditLineConstString("Fix line indentation"), (EditlineCommandCallbackType)([](EditLine *editline, int ch) { return Editline::InstanceFor (editline)->FixIndentationCommand (ch); })); // Register the complete callback under two names for compatibility with older clients using // custom .editrc files (largely becuase libedit has a bad bug where if you have a bind command // that tries to bind to a function name that doesn't exist, it can corrupt the heap and // crash your process later.) EditlineCommandCallbackType complete_callback = [] (EditLine * editline, int ch) { return Editline::InstanceFor (editline)->TabCommand (ch); }; el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-complete"), EditLineConstString("Invoke completion"), complete_callback); el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb_complete"), EditLineConstString("Invoke completion"), complete_callback); // General bindings we don't mind being overridden if (!multiline) { el_set (m_editline, EL_BIND, "^r", "em-inc-search-prev", NULL); // Cycle through backwards search, entering string } 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", NULL); // Bind TAB to auto complete // Allow user-specific customization prior to registering bindings we absolutely require el_source (m_editline, NULL); // Register an internal binding that external developers shouldn't use el_wset(m_editline, EL_ADDFN, EditLineConstString("lldb-revert-line"), EditLineConstString("Revert line to saved state"), (EditlineCommandCallbackType)( [](EditLine *editline, int ch) { return Editline::InstanceFor(editline)->RevertLineCommand(ch); })); // Register keys that perform auto-indent correction if (m_fix_indentation_callback && m_fix_indentation_callback_chars) { char bind_key[2] = { 0, 0 }; const char * indent_chars = m_fix_indentation_callback_chars; while (*indent_chars) { bind_key[0] = *indent_chars; el_set (m_editline, EL_BIND, bind_key, "lldb-fix-indentation", NULL); ++indent_chars; } } // Multi-line editor bindings if (multiline) { el_set(m_editline, EL_BIND, "\n", "lldb-end-or-add-line", NULL); el_set(m_editline, EL_BIND, "\r", "lldb-end-or-add-line", NULL); el_set(m_editline, EL_BIND, ESCAPE "\n", "lldb-break-line", NULL); el_set(m_editline, EL_BIND, ESCAPE "\r", "lldb-break-line", NULL); el_set (m_editline, EL_BIND, "^p", "lldb-previous-line", NULL); el_set (m_editline, EL_BIND, "^n", "lldb-next-line", NULL); el_set (m_editline, EL_BIND, "^?", "lldb-delete-previous-char", NULL); el_set (m_editline, EL_BIND, "^d", "lldb-delete-next-char", NULL); el_set (m_editline, EL_BIND, ESCAPE "[3~", "lldb-delete-next-char", NULL); el_set (m_editline, EL_BIND, ESCAPE "[\\^", "lldb-revert-line", NULL); // Editor-specific bindings if (IsEmacs()) { el_set (m_editline, EL_BIND, ESCAPE "<", "lldb-buffer-start", NULL); el_set (m_editline, EL_BIND, ESCAPE ">", "lldb-buffer-end", NULL); el_set (m_editline, EL_BIND, ESCAPE "[A", "lldb-previous-line", NULL); el_set (m_editline, EL_BIND, ESCAPE "[B", "lldb-next-line", NULL); el_set(m_editline, EL_BIND, ESCAPE ESCAPE "[A", "lldb-previous-history", NULL); el_set(m_editline, EL_BIND, ESCAPE ESCAPE "[B", "lldb-next-history", NULL); el_set(m_editline, EL_BIND, ESCAPE "[1;3A", "lldb-previous-history", NULL); el_set(m_editline, EL_BIND, ESCAPE "[1;3B", "lldb-next-history", NULL); } else { el_set (m_editline, EL_BIND, "^H", "lldb-delete-previous-char", NULL); el_set (m_editline, EL_BIND, "-a", ESCAPE "[A", "lldb-previous-line", NULL); el_set (m_editline, EL_BIND, "-a", ESCAPE "[B", "lldb-next-line", NULL); el_set (m_editline, EL_BIND, "-a", "x", "lldb-delete-next-char", NULL); el_set (m_editline, EL_BIND, "-a", "^H", "lldb-delete-previous-char", NULL); el_set (m_editline, EL_BIND, "-a", "^?", "lldb-delete-previous-char", NULL); // Escape is absorbed exiting edit mode, so re-register important sequences // without the prefix el_set (m_editline, EL_BIND, "-a", "[A", "lldb-previous-line", NULL); el_set (m_editline, EL_BIND, "-a", "[B", "lldb-next-line", NULL); el_set (m_editline, EL_BIND, "-a", "[\\^", "lldb-revert-line", NULL); } } } //------------------------------------------------------------------ // Editline public methods //------------------------------------------------------------------ Editline * Editline::InstanceFor (EditLine * editline) { Editline * editor; el_get (editline, EL_CLIENTDATA, &editor); return editor; } Editline::Editline (const char * editline_name, FILE * input_file, FILE * output_file, FILE * error_file, bool color_prompts) : m_editor_status (EditorStatus::Complete), m_color_prompts(color_prompts), m_input_file (input_file), m_output_file (output_file), m_error_file (error_file), m_input_connection (fileno(input_file), false) { // Get a shared history instance m_editor_name = (editline_name == nullptr) ? "lldb-tmp" : editline_name; m_history_sp = EditlineHistory::GetHistory (m_editor_name); #ifdef USE_SETUPTERM_WORKAROUND if (m_output_file) { const int term_fd = fileno(m_output_file); if (term_fd != -1) { static std::mutex *g_init_terminal_fds_mutex_ptr = nullptr; static std::set *g_init_terminal_fds_ptr = nullptr; static std::once_flag g_once_flag; std::call_once(g_once_flag, [&]() { g_init_terminal_fds_mutex_ptr = new std::mutex(); // NOTE: Leak to avoid C++ destructor chain issues g_init_terminal_fds_ptr = new std::set(); // NOTE: Leak to avoid C++ destructor chain issues }); // We must make sure to initialize the terminal a given file descriptor // only once. If we do this multiple times, we start leaking memory. std::lock_guard guard(*g_init_terminal_fds_mutex_ptr); if (g_init_terminal_fds_ptr->find(term_fd) == g_init_terminal_fds_ptr->end()) { g_init_terminal_fds_ptr->insert(term_fd); setupterm((char *)0, term_fd, (int *)0); } } } #endif } Editline::~Editline() { if (m_editline) { // Disable edit mode to stop the terminal from flushing all input // during the call to el_end() since we expect to have multiple editline // instances in this program. el_set (m_editline, EL_EDITMODE, 0); el_end (m_editline); m_editline = nullptr; } // EditlineHistory objects are sometimes shared between multiple // Editline instances with the same program name. So just release // our shared pointer and if we are the last owner, it will save the // history to the history save file automatically. m_history_sp.reset(); } void Editline::SetPrompt (const char * prompt) { m_set_prompt = prompt == nullptr ? "" : prompt; } void Editline::SetContinuationPrompt (const char * continuation_prompt) { m_set_continuation_prompt = continuation_prompt == nullptr ? "" : continuation_prompt; } void Editline::TerminalSizeChanged() { if (m_editline != nullptr) { el_resize (m_editline); int columns; // Despite the man page claiming non-zero indicates success, it's actually zero if (el_get (m_editline, EL_GETTC, "co", &columns) == 0) { m_terminal_width = columns; if (m_current_line_rows != -1) { const LineInfoW * info = el_wline (m_editline); int lineLength = (int)((info->lastchar - info->buffer) + GetPromptWidth()); m_current_line_rows = (lineLength / columns) + 1; } } else { m_terminal_width = INT_MAX; m_current_line_rows = 1; } } } const char * Editline::GetPrompt() { return m_set_prompt.c_str(); } uint32_t Editline::GetCurrentLine() { return m_current_line_index; } bool Editline::Interrupt() { bool result = true; std::lock_guard guard(m_output_mutex); if (m_editor_status == EditorStatus::Editing) { fprintf(m_output_file, "^C\n"); result = m_input_connection.InterruptRead(); } m_editor_status = EditorStatus::Interrupted; return result; } bool Editline::Cancel() { bool result = true; std::lock_guard guard(m_output_mutex); if (m_editor_status == EditorStatus::Editing) { MoveCursor(CursorLocation::EditingCursor, CursorLocation::BlockStart); fprintf(m_output_file, ANSI_CLEAR_BELOW); result = m_input_connection.InterruptRead(); } m_editor_status = EditorStatus::Interrupted; return result; } void Editline::SetAutoCompleteCallback (CompleteCallbackType callback, void * baton) { m_completion_callback = callback; m_completion_callback_baton = baton; } void Editline::SetIsInputCompleteCallback (IsInputCompleteCallbackType callback, void * baton) { m_is_input_complete_callback = callback; m_is_input_complete_callback_baton = baton; } bool Editline::SetFixIndentationCallback (FixIndentationCallbackType callback, void * baton, const char * indent_chars) { m_fix_indentation_callback = callback; m_fix_indentation_callback_baton = baton; m_fix_indentation_callback_chars = indent_chars; return false; } bool Editline::GetLine (std::string &line, bool &interrupted) { ConfigureEditor (false); m_input_lines = std::vector(); m_input_lines.insert (m_input_lines.begin(), EditLineConstString("")); std::lock_guard guard(m_output_mutex); lldbassert(m_editor_status != EditorStatus::Editing); if (m_editor_status == EditorStatus::Interrupted) { m_editor_status = EditorStatus::Complete; interrupted = true; return true; } SetCurrentLine (0); m_in_history = false; m_editor_status = EditorStatus::Editing; m_revert_cursor_index = -1; int count; auto input = el_wgets (m_editline, &count); interrupted = m_editor_status == EditorStatus::Interrupted; if (!interrupted) { if (input == nullptr) { fprintf (m_output_file, "\n"); m_editor_status = EditorStatus::EndOfInput; } else { m_history_sp->Enter (input); #if LLDB_EDITLINE_USE_WCHAR line = m_utf8conv.to_bytes (SplitLines (input)[0]); #else line = SplitLines (input)[0]; #endif m_editor_status = EditorStatus::Complete; } } return m_editor_status != EditorStatus::EndOfInput; } bool Editline::GetLines (int first_line_number, StringList &lines, bool &interrupted) { ConfigureEditor (true); // Print the initial input lines, then move the cursor back up to the start of input SetBaseLineNumber (first_line_number); m_input_lines = std::vector(); m_input_lines.insert (m_input_lines.begin(), EditLineConstString("")); std::lock_guard guard(m_output_mutex); // Begin the line editing loop DisplayInput(); SetCurrentLine (0); MoveCursor (CursorLocation::BlockEnd, CursorLocation::BlockStart); m_editor_status = EditorStatus::Editing; m_in_history = false; m_revert_cursor_index = -1; while (m_editor_status == EditorStatus::Editing) { int count; m_current_line_rows = -1; el_wpush (m_editline, EditLineConstString("\x1b[^")); // Revert to the existing line content el_wgets (m_editline, &count); } interrupted = m_editor_status == EditorStatus::Interrupted; if (!interrupted) { // Save the completed entry in history before returning m_history_sp->Enter (CombineLines (m_input_lines).c_str()); lines = GetInputAsStringList(); } return m_editor_status != EditorStatus::EndOfInput; } void Editline::PrintAsync (Stream *stream, const char *s, size_t len) { std::lock_guard guard(m_output_mutex); if (m_editor_status == EditorStatus::Editing) { MoveCursor(CursorLocation::EditingCursor, CursorLocation::BlockStart); fprintf(m_output_file, ANSI_CLEAR_BELOW); } stream->Write (s, len); stream->Flush(); if (m_editor_status == EditorStatus::Editing) { DisplayInput(); MoveCursor(CursorLocation::BlockEnd, CursorLocation::EditingCursor); } }