llvm-project/lldb/source/Host/common/Editline.cpp

1378 lines
46 KiB
C++

//===-- 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 <iomanip>
#include <iostream>
#include <limits.h>
#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/Host/Mutex.h"
using namespace lldb_private;
using namespace lldb_private::line_editor;
// 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<EditLineStringType> & lines)
{
EditLineStringStreamType combined_stream;
for (EditLineStringType line : lines)
{
combined_stream << line.c_str() << "\n";
}
return combined_stream.str();
}
std::vector<EditLineStringType>
SplitLines (const EditLineStringType & input)
{
std::vector<EditLineStringType> 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<EditlineHistory> 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())
{
std::string parent_path = FileSpec ("~/.lldb", true).GetPath();
char history_path[PATH_MAX];
if (FileSystem::MakeDirectory(parent_path.c_str(), 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 = std::move (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<std::string, EditlineHistoryWP> WeakHistoryMap;
static Mutex g_mutex (Mutex::eMutexTypeRecursive);
static WeakHistoryMap g_weak_map;
Mutex::Locker locker (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<EditLineStringType> 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;
m_editor_getting_char = true;
int read_count = m_input_connection.Read(&ch, 1, UINT32_MAX, status, NULL);
m_editor_getting_char = false;
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(*c != EOF)
return 1;
#endif
}
else
{
switch (status)
{
case lldb::eConnectionStatusInterrupted:
m_editor_status = EditorStatus::Interrupted;
printf ("^C\n");
return 0;
case lldb::eConnectionStatusSuccess: // Success
break;
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 end of input detection or automatic formatting when pasting
if (!IsInputPending (m_input_file))
{
// If this is the end of the last line, treat this as a potential exit
if (m_current_line_index == m_input_lines.size() - 1 && new_line_fragment.length() == 0)
{
bool end_of_input = true;
if (m_is_input_complete_callback)
{
SaveEditedLine();
auto lines = GetInputAsStringList();
end_of_input = m_is_input_complete_callback (this, lines, m_is_input_complete_callback_baton);
// The completion test is allowed to change the input lines when complete
if (end_of_input)
{
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
}
}
}
if (end_of_input)
{
fprintf (m_output_file, "\n");
m_editor_status = EditorStatus::Complete;
return CC_NEWLINE;
}
}
// 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::DeleteNextCharCommand (int ch)
{
LineInfoW * info = (LineInfoW *)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 = (LineInfoW *)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::FixIndentationCommand (int ch)
{
if (!m_fix_indentation_callback)
return CC_NORM;
// Insert the character by hand prior to correction
EditLineCharType inserted[] = { (EditLineCharType)ch, 0 };
el_winsertstr (m_editline, inserted);
SaveEditedLine();
StringList lines = GetInputAsStringList (m_current_line_index + 1);
// Determine the cursor position
LineInfoW * info = (LineInfoW *)el_wline (m_editline);
int cursor_position = info->cursor - info->buffer;
int indent_correction = m_fix_indentation_callback (this, lines, cursor_position, m_fix_indentation_callback_baton);
// Adjust the input buffer to correct indentation
if (indent_correction > 0)
{
info->cursor = info->buffer;
el_winsertstr (m_editline, EditLineStringType (indent_correction, EditLineCharType(' ')).c_str());
}
else if (indent_correction < 0)
{
info->cursor = info->buffer - indent_correction;
el_wdeletestr (m_editline, -indent_correction);
}
info->cursor = info->buffer + cursor_position + indent_correction;
return CC_REFRESH;
}
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 = (LineInfoW *)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_set (m_editline, EL_ADDFN, "lldb-break-line", "Insert a line break",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->BreakLineCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-delete-next-char", "Delete next character",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->DeleteNextCharCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-delete-previous-char", "Delete previous character",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->DeletePreviousCharCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-previous-line", "Move to previous line",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->PreviousLineCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-next-line", "Move to next line",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->NextLineCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-buffer-start", "Move to start of buffer",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->BufferStartCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-buffer-end", "Move to end of buffer",
(EditlineCommandCallbackType)([] (EditLine * editline, int ch) {
return Editline::InstanceFor (editline)->BufferEndCommand (ch);
}));
el_set (m_editline, EL_ADDFN, "lldb-fix-indentation", "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_set (m_editline, EL_ADDFN, "lldb-complete", "Invoke completion", complete_callback);
el_set (m_editline, EL_ADDFN, "lldb_complete", "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_set (m_editline, EL_ADDFN, "lldb-revert-line", "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-break-line", NULL);
el_set (m_editline, EL_BIND, "\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);
}
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);
}
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;
}
void
Editline::Hide()
{
// Make sure we're at a stable location waiting for input
while (m_editor_status == EditorStatus::Editing && !m_editor_getting_char)
{
usleep(100000);
}
// Clear the existing input
if (m_editor_status == EditorStatus::Editing)
{
MoveCursor(CursorLocation::EditingCursor, CursorLocation::BlockStart);
fprintf(m_output_file, ANSI_CLEAR_BELOW);
}
}
void
Editline::Refresh()
{
if (m_editor_status == EditorStatus::Editing)
{
DisplayInput();
MoveCursor(CursorLocation::BlockEnd, CursorLocation::EditingCursor);
}
}
bool
Editline::Interrupt()
{
if (m_editor_status == EditorStatus::Editing)
{
return m_input_connection.InterruptRead();
}
return false; // Interrupt not handled as we weren't getting a line or lines
}
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<EditLineStringType>();
m_input_lines.insert (m_input_lines.begin(), EditLineConstString(""));
SetCurrentLine (0);
m_in_history = false;
m_editor_status = EditorStatus::Editing;
m_editor_getting_char = false;
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<EditLineStringType>();
m_input_lines.insert (m_input_lines.begin(), EditLineConstString(""));
// Begin the line editing loop
DisplayInput();
SetCurrentLine (0);
MoveCursor (CursorLocation::BlockEnd, CursorLocation::BlockStart);
m_editor_status = EditorStatus::Editing;
m_editor_getting_char = false;
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;
}