Although the interface to el_push should be a constant parameter (as it is on Darwin), certain Linux distributions currently ship a header which does not provide proper const correctness. This causes compilation failures on Linux. Strip the constness on the parameter, which whilst incorrect, is mostly harmless. The parameter will not be changed by the interface and so it is acceptable to do this. When distributions have updated to a more correct declaration, it would be nice to revert this change. Addresses PR18784. llvm-svn: 201092
692 lines
20 KiB
C++
692 lines
20 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 "lldb/Host/Editline.h"
|
|
|
|
#include "lldb/Core/Error.h"
|
|
#include "lldb/Core/StreamString.h"
|
|
#include "lldb/Core/StringList.h"
|
|
#include "lldb/Host/Host.h"
|
|
|
|
#include <limits.h>
|
|
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
|
|
static const char k_prompt_escape_char = '\1';
|
|
|
|
Editline::Editline (const char *prog, // prog can't be NULL
|
|
const char *prompt, // can be NULL for no prompt
|
|
FILE *fin,
|
|
FILE *fout,
|
|
FILE *ferr) :
|
|
m_editline (NULL),
|
|
m_history (NULL),
|
|
m_history_event (),
|
|
m_program (),
|
|
m_prompt (),
|
|
m_lines_prompt (),
|
|
m_getc_buffer (),
|
|
m_getc_mutex (Mutex::eMutexTypeNormal),
|
|
m_getc_cond (),
|
|
// m_gets_mutex (Mutex::eMutexTypeNormal),
|
|
m_completion_callback (NULL),
|
|
m_completion_callback_baton (NULL),
|
|
m_line_complete_callback (NULL),
|
|
m_line_complete_callback_baton (NULL),
|
|
m_lines_command (Command::None),
|
|
m_lines_curr_line (0),
|
|
m_lines_max_line (0),
|
|
m_prompt_with_line_numbers (false),
|
|
m_getting_line (false),
|
|
m_got_eof (false),
|
|
m_interrupted (false)
|
|
{
|
|
if (prog && prog[0])
|
|
{
|
|
m_program = prog;
|
|
m_editline = ::el_init(prog, fin, fout, ferr);
|
|
m_history = ::history_init();
|
|
}
|
|
else
|
|
{
|
|
m_editline = ::el_init("lldb-tmp", fin, fout, ferr);
|
|
}
|
|
if (prompt && prompt[0])
|
|
SetPrompt (prompt);
|
|
|
|
//::el_set (m_editline, EL_BIND, "^[[A", NULL); // Print binding for up arrow key
|
|
//::el_set (m_editline, EL_BIND, "^[[B", NULL); // Print binding for up down key
|
|
|
|
assert (m_editline);
|
|
::el_set (m_editline, EL_CLIENTDATA, this);
|
|
#ifdef EL_PROMPT_ESC
|
|
::el_set (m_editline, EL_PROMPT_ESC, GetPromptCallback, k_prompt_escape_char);
|
|
#endif
|
|
::el_set (m_editline, EL_EDITOR, "emacs");
|
|
if (m_history)
|
|
{
|
|
::el_set (m_editline, EL_HIST, history, m_history);
|
|
}
|
|
::el_set (m_editline, EL_ADDFN, "lldb-complete", "Editline completion function", Editline::CallbackComplete);
|
|
::el_set (m_editline, EL_ADDFN, "lldb-edit-prev-line", "Editline edit prev line", Editline::CallbackEditPrevLine);
|
|
::el_set (m_editline, EL_ADDFN, "lldb-edit-next-line", "Editline edit next line", Editline::CallbackEditNextLine);
|
|
|
|
::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 does.
|
|
::el_set (m_editline, EL_BIND, "\033[3~", "ed-delete-next-char", NULL); // Fix the delete key.
|
|
::el_set (m_editline, EL_BIND, "\t", "lldb-complete", NULL); // Bind TAB to be autocompelte
|
|
|
|
// Source $PWD/.editrc then $HOME/.editrc
|
|
::el_source (m_editline, NULL);
|
|
|
|
if (m_history)
|
|
{
|
|
::history (m_history, &m_history_event, H_SETSIZE, 800);
|
|
::history (m_history, &m_history_event, H_SETUNIQUE, 1);
|
|
}
|
|
|
|
// Always read through our callback function so we don't read
|
|
// stuff we aren't supposed to. This also stops the extra echoing
|
|
// that can happen when you have more input than editline can handle
|
|
// at once.
|
|
SetGetCharCallback(GetCharFromInputFileCallback);
|
|
|
|
LoadHistory();
|
|
}
|
|
|
|
Editline::~Editline()
|
|
{
|
|
SaveHistory();
|
|
|
|
if (m_history)
|
|
{
|
|
::history_end (m_history);
|
|
m_history = NULL;
|
|
}
|
|
|
|
// 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 = NULL;
|
|
}
|
|
|
|
void
|
|
Editline::SetGetCharCallback (GetCharCallbackType callback)
|
|
{
|
|
::el_set (m_editline, EL_GETCFN, callback);
|
|
}
|
|
|
|
FileSpec
|
|
Editline::GetHistoryFile()
|
|
{
|
|
char history_path[PATH_MAX];
|
|
::snprintf (history_path, sizeof(history_path), "~/.%s-history", m_program.c_str());
|
|
return FileSpec(history_path, true);
|
|
}
|
|
|
|
bool
|
|
Editline::LoadHistory ()
|
|
{
|
|
if (m_history)
|
|
{
|
|
FileSpec history_file(GetHistoryFile());
|
|
if (history_file.Exists())
|
|
::history (m_history, &m_history_event, H_LOAD, history_file.GetPath().c_str());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
Editline::SaveHistory ()
|
|
{
|
|
if (m_history)
|
|
{
|
|
std::string history_path = GetHistoryFile().GetPath();
|
|
::history (m_history, &m_history_event, H_SAVE, history_path.c_str());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
Error
|
|
Editline::PrivateGetLine(std::string &line)
|
|
{
|
|
Error error;
|
|
if (m_interrupted)
|
|
{
|
|
error.SetErrorString("interrupted");
|
|
return error;
|
|
}
|
|
|
|
line.clear();
|
|
if (m_editline != NULL)
|
|
{
|
|
int line_len = 0;
|
|
const char *line_cstr = NULL;
|
|
// Call el_gets to prompt the user and read the user's input.
|
|
// {
|
|
// // Make sure we know when we are in el_gets() by using a mutex
|
|
// Mutex::Locker locker (m_gets_mutex);
|
|
line_cstr = ::el_gets (m_editline, &line_len);
|
|
// }
|
|
|
|
static int save_errno = (line_len < 0) ? errno : 0;
|
|
|
|
if (save_errno != 0)
|
|
{
|
|
error.SetError(save_errno, eErrorTypePOSIX);
|
|
}
|
|
else if (line_cstr)
|
|
{
|
|
// Decrement the length so we don't have newline characters in "line" for when
|
|
// we assign the cstr into the std::string
|
|
while (line_len > 0 &&
|
|
(line_cstr[line_len - 1] == '\n' ||
|
|
line_cstr[line_len - 1] == '\r'))
|
|
--line_len;
|
|
|
|
if (line_len > 0)
|
|
{
|
|
// We didn't strip the newlines, we just adjusted the length, and
|
|
// we want to add the history item with the newlines
|
|
if (m_history)
|
|
::history (m_history, &m_history_event, H_ENTER, line_cstr);
|
|
|
|
// Copy the part of the c string that we want (removing the newline chars)
|
|
line.assign(line_cstr, line_len);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
error.SetErrorString("the EditLine instance has been deleted");
|
|
}
|
|
return error;
|
|
}
|
|
|
|
|
|
Error
|
|
Editline::GetLine(std::string &line)
|
|
{
|
|
Error error;
|
|
line.clear();
|
|
|
|
// Set arrow key bindings for up and down arrows for single line
|
|
// mode where up and down arrows do prev/next history
|
|
::el_set (m_editline, EL_BIND, "^[[A", "ed-prev-history", NULL); // Map up arrow
|
|
::el_set (m_editline, EL_BIND, "^[[B", "ed-next-history", NULL); // Map down arrow
|
|
m_interrupted = false;
|
|
|
|
if (!m_got_eof)
|
|
{
|
|
if (m_getting_line)
|
|
{
|
|
error.SetErrorString("already getting a line");
|
|
return error;
|
|
}
|
|
if (m_lines_curr_line > 0)
|
|
{
|
|
error.SetErrorString("already getting lines");
|
|
return error;
|
|
}
|
|
m_getting_line = true;
|
|
error = PrivateGetLine(line);
|
|
m_getting_line = false;
|
|
}
|
|
|
|
if (m_got_eof && line.empty())
|
|
{
|
|
// Only set the error if we didn't get an error back from PrivateGetLine()
|
|
if (error.Success())
|
|
error.SetErrorString("end of file");
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
size_t
|
|
Editline::Push (const char *bytes, size_t len)
|
|
{
|
|
if (m_editline)
|
|
{
|
|
// Must NULL terminate the string for el_push() so we stick it
|
|
// into a std::string first
|
|
::el_push(m_editline,
|
|
const_cast<char*>(std::string (bytes, len).c_str()));
|
|
return len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
Error
|
|
Editline::GetLines(const std::string &end_line, StringList &lines)
|
|
{
|
|
Error error;
|
|
if (m_getting_line)
|
|
{
|
|
error.SetErrorString("already getting a line");
|
|
return error;
|
|
}
|
|
if (m_lines_curr_line > 0)
|
|
{
|
|
error.SetErrorString("already getting lines");
|
|
return error;
|
|
}
|
|
|
|
// Set arrow key bindings for up and down arrows for multiple line
|
|
// mode where up and down arrows do edit prev/next line
|
|
::el_set (m_editline, EL_BIND, "^[[A", "lldb-edit-prev-line", NULL); // Map up arrow
|
|
::el_set (m_editline, EL_BIND, "^[[B", "lldb-edit-next-line", NULL); // Map down arrow
|
|
::el_set (m_editline, EL_BIND, "^b", "ed-prev-history", NULL);
|
|
::el_set (m_editline, EL_BIND, "^n", "ed-next-history", NULL);
|
|
m_interrupted = false;
|
|
|
|
LineStatus line_status = LineStatus::Success;
|
|
|
|
lines.Clear();
|
|
|
|
FILE *out_file = GetOutputFile();
|
|
FILE *err_file = GetErrorFile();
|
|
m_lines_curr_line = 1;
|
|
while (line_status != LineStatus::Done)
|
|
{
|
|
const uint32_t line_idx = m_lines_curr_line-1;
|
|
if (line_idx >= lines.GetSize())
|
|
lines.SetSize(m_lines_curr_line);
|
|
m_lines_max_line = lines.GetSize();
|
|
m_lines_command = Command::None;
|
|
assert(line_idx < m_lines_max_line);
|
|
std::string &line = lines[line_idx];
|
|
error = PrivateGetLine(line);
|
|
if (error.Fail())
|
|
{
|
|
line_status = LineStatus::Error;
|
|
}
|
|
else
|
|
{
|
|
switch (m_lines_command)
|
|
{
|
|
case Command::None:
|
|
if (m_line_complete_callback)
|
|
{
|
|
line_status = m_line_complete_callback (this,
|
|
lines,
|
|
line_idx,
|
|
error,
|
|
m_line_complete_callback_baton);
|
|
}
|
|
else if (line == end_line)
|
|
{
|
|
line_status = LineStatus::Done;
|
|
}
|
|
|
|
if (line_status == LineStatus::Success)
|
|
{
|
|
++m_lines_curr_line;
|
|
// If we already have content for the next line because
|
|
// we were editing previous lines, then populate the line
|
|
// with the appropriate contents
|
|
if (line_idx+1 < lines.GetSize() && !lines[line_idx+1].empty())
|
|
::el_push (m_editline,
|
|
const_cast<char*>(lines[line_idx+1].c_str()));
|
|
}
|
|
else if (line_status == LineStatus::Error)
|
|
{
|
|
// Clear to end of line ("ESC[K"), then print the error,
|
|
// then go to the next line ("\n") and then move cursor up
|
|
// two lines ("ESC[2A").
|
|
fprintf (err_file, "\033[Kerror: %s\n\033[2A", error.AsCString());
|
|
}
|
|
break;
|
|
case Command::EditPrevLine:
|
|
if (m_lines_curr_line > 1)
|
|
{
|
|
//::fprintf (out_file, "\033[1A\033[%uD\033[2K", (uint32_t)(m_lines_prompt.size() + lines[line_idx].size())); // Make cursor go up a line and clear that line
|
|
::fprintf (out_file, "\033[1A\033[1000D\033[2K");
|
|
if (!lines[line_idx-1].empty())
|
|
::el_push (m_editline,
|
|
const_cast<char*>(lines[line_idx-1].c_str()));
|
|
--m_lines_curr_line;
|
|
}
|
|
break;
|
|
case Command::EditNextLine:
|
|
// Allow the down arrow to create a new line
|
|
++m_lines_curr_line;
|
|
//::fprintf (out_file, "\033[1B\033[%uD\033[2K", (uint32_t)(m_lines_prompt.size() + lines[line_idx].size()));
|
|
::fprintf (out_file, "\033[1B\033[1000D\033[2K");
|
|
if (line_idx+1 < lines.GetSize() && !lines[line_idx+1].empty())
|
|
::el_push (m_editline,
|
|
const_cast<char*>(lines[line_idx+1].c_str()));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
m_lines_curr_line = 0;
|
|
m_lines_command = Command::None;
|
|
|
|
// If we have a callback, call it one more time to let the
|
|
// user know the lines are complete
|
|
if (m_line_complete_callback)
|
|
m_line_complete_callback (this,
|
|
lines,
|
|
UINT32_MAX,
|
|
error,
|
|
m_line_complete_callback_baton);
|
|
|
|
return error;
|
|
}
|
|
|
|
unsigned char
|
|
Editline::HandleCompletion (int ch)
|
|
{
|
|
if (m_completion_callback == NULL)
|
|
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);
|
|
|
|
FILE *out_file = GetOutputFile();
|
|
|
|
// 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 != NULL && *completion_str != '\0')
|
|
{
|
|
el_insertstr (m_editline, completion_str);
|
|
return CC_REDISPLAY;
|
|
}
|
|
|
|
if (num_completions > 1)
|
|
{
|
|
int num_elements = num_completions + 1;
|
|
::fprintf (out_file, "\nAvailable completions:");
|
|
if (num_completions < page_size)
|
|
{
|
|
for (int i = 1; i < num_elements; i++)
|
|
{
|
|
completion_str = completions.GetStringAtIndex(i);
|
|
::fprintf (out_file, "\n\t%s", completion_str);
|
|
}
|
|
::fprintf (out_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 (out_file, "\n\t%s", completion_str);
|
|
}
|
|
|
|
if (cur_pos >= num_elements)
|
|
{
|
|
::fprintf (out_file, "\n");
|
|
break;
|
|
}
|
|
|
|
::fprintf (out_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;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (num_completions == 0)
|
|
return CC_REFRESH_BEEP;
|
|
else
|
|
return CC_REDISPLAY;
|
|
}
|
|
|
|
Editline *
|
|
Editline::GetClientData (::EditLine *e)
|
|
{
|
|
Editline *editline = NULL;
|
|
if (e && ::el_get(e, EL_CLIENTDATA, &editline) == 0)
|
|
return editline;
|
|
return NULL;
|
|
}
|
|
|
|
FILE *
|
|
Editline::GetInputFile ()
|
|
{
|
|
return GetFilePointer (m_editline, 0);
|
|
}
|
|
|
|
FILE *
|
|
Editline::GetOutputFile ()
|
|
{
|
|
return GetFilePointer (m_editline, 1);
|
|
}
|
|
|
|
FILE *
|
|
Editline::GetErrorFile ()
|
|
{
|
|
return GetFilePointer (m_editline, 2);
|
|
}
|
|
|
|
const char *
|
|
Editline::GetPrompt()
|
|
{
|
|
if (m_prompt_with_line_numbers && m_lines_curr_line > 0)
|
|
{
|
|
StreamString strm;
|
|
strm.Printf("%3u: ", m_lines_curr_line);
|
|
m_lines_prompt = std::move(strm.GetString());
|
|
return m_lines_prompt.c_str();
|
|
}
|
|
else
|
|
{
|
|
return m_prompt.c_str();
|
|
}
|
|
}
|
|
|
|
void
|
|
Editline::SetPrompt (const char *p)
|
|
{
|
|
if (p && p[0])
|
|
m_prompt = p;
|
|
else
|
|
m_prompt.clear();
|
|
size_t start_pos = 0;
|
|
size_t escape_pos;
|
|
while ((escape_pos = m_prompt.find('\033', start_pos)) != std::string::npos)
|
|
{
|
|
m_prompt.insert(escape_pos, 1, k_prompt_escape_char);
|
|
start_pos += 2;
|
|
}
|
|
}
|
|
|
|
FILE *
|
|
Editline::GetFilePointer (::EditLine *e, int fd)
|
|
{
|
|
FILE *file_ptr = NULL;
|
|
if (e && ::el_get(e, EL_GETFP, fd, &file_ptr) == 0)
|
|
return file_ptr;
|
|
return NULL;
|
|
}
|
|
|
|
unsigned char
|
|
Editline::CallbackEditPrevLine (::EditLine *e, int ch)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline->m_lines_curr_line > 1)
|
|
{
|
|
editline->m_lines_command = Command::EditPrevLine;
|
|
return CC_NEWLINE;
|
|
}
|
|
return CC_ERROR;
|
|
}
|
|
unsigned char
|
|
Editline::CallbackEditNextLine (::EditLine *e, int ch)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline->m_lines_curr_line < editline->m_lines_max_line)
|
|
{
|
|
editline->m_lines_command = Command::EditNextLine;
|
|
return CC_NEWLINE;
|
|
}
|
|
return CC_ERROR;
|
|
}
|
|
|
|
unsigned char
|
|
Editline::CallbackComplete (::EditLine *e, int ch)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline)
|
|
return editline->HandleCompletion (ch);
|
|
return CC_ERROR;
|
|
}
|
|
|
|
const char *
|
|
Editline::GetPromptCallback (::EditLine *e)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline)
|
|
return editline->GetPrompt();
|
|
return "";
|
|
}
|
|
|
|
size_t
|
|
Editline::SetInputBuffer (const char *c, size_t len)
|
|
{
|
|
if (c && len > 0)
|
|
{
|
|
Mutex::Locker locker(m_getc_mutex);
|
|
SetGetCharCallback(GetCharInputBufferCallback);
|
|
m_getc_buffer.append(c, len);
|
|
m_getc_cond.Broadcast();
|
|
}
|
|
return len;
|
|
}
|
|
|
|
int
|
|
Editline::GetChar (char *c)
|
|
{
|
|
Mutex::Locker locker(m_getc_mutex);
|
|
if (m_getc_buffer.empty())
|
|
m_getc_cond.Wait(m_getc_mutex);
|
|
if (m_getc_buffer.empty())
|
|
return 0;
|
|
*c = m_getc_buffer[0];
|
|
m_getc_buffer.erase(0,1);
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
Editline::GetCharInputBufferCallback (EditLine *e, char *c)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline)
|
|
return editline->GetChar(c);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
Editline::GetCharFromInputFileCallback (EditLine *e, char *c)
|
|
{
|
|
Editline *editline = GetClientData (e);
|
|
if (editline && editline->m_got_eof == false)
|
|
{
|
|
char ch = ::fgetc(editline->GetInputFile());
|
|
if (ch == '\x04')
|
|
{
|
|
// Only turn a CTRL+D into a EOF if we receive the
|
|
// CTRL+D an empty line, otherwise it will forward
|
|
// delete the character at the cursor
|
|
const LineInfo *line_info = ::el_line(e);
|
|
if (line_info != NULL &&
|
|
line_info->buffer == line_info->cursor &&
|
|
line_info->cursor == line_info->lastchar)
|
|
{
|
|
ch = EOF;
|
|
}
|
|
}
|
|
|
|
if (ch == EOF)
|
|
{
|
|
editline->m_got_eof = true;
|
|
}
|
|
else
|
|
{
|
|
*c = ch;
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
Editline::Hide ()
|
|
{
|
|
FILE *out_file = GetOutputFile();
|
|
if (out_file)
|
|
{
|
|
const LineInfo *line_info = ::el_line(m_editline);
|
|
if (line_info)
|
|
::fprintf (out_file, "\033[%uD\033[K", (uint32_t)(strlen(GetPrompt()) + line_info->cursor - line_info->buffer));
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
Editline::Refresh()
|
|
{
|
|
::el_set (m_editline, EL_REFRESH);
|
|
}
|
|
|
|
void
|
|
Editline::Interrupt ()
|
|
{
|
|
m_interrupted = true;
|
|
if (m_getting_line || m_lines_curr_line > 0)
|
|
el_insertstr(m_editline, "\n"); // True to force the line to complete itself so we get exit from el_gets()
|
|
}
|