Files
email-tracker/external/duckdb/tools/shell/linenoise/linenoise.cpp
2025-10-24 19:21:19 -05:00

1623 lines
41 KiB
C++

#include <sys/stat.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "linenoise.h"
#include "linenoise.hpp"
#include "history.hpp"
#include "highlighting.hpp"
#include "terminal.hpp"
#include "utf8proc_wrapper.hpp"
#include <unordered_set>
#include <vector>
#include "duckdb_shell_wrapper.h"
#include <string>
#include "sqlite3.h"
#include "duckdb/common/string_util.hpp"
#ifdef __MVS__
#include <strings.h>
#include <sys/time.h>
#endif
#ifdef LINENOISE_EDITOR
#if defined(WIN32) || defined(__CYGWIN__)
#define DEFAULT_EDITOR "notepad.exe"
#else
#define DEFAULT_EDITOR "vi"
#endif
#endif
namespace duckdb {
static linenoiseCompletionCallback *completionCallback = NULL;
static linenoiseHintsCallback *hintsCallback = NULL;
static linenoiseFreeHintsCallback *freeHintsCallback = NULL;
int linenoiseHistoryAdd(const char *line);
/* ============================== Completion ================================ */
/* Free a list of completion option populated by linenoiseAddCompletion(). */
static void freeCompletions(linenoiseCompletions *lc) {
size_t i;
for (i = 0; i < lc->len; i++) {
free(lc->cvec[i]);
}
if (lc->cvec != nullptr) {
free(lc->cvec);
}
}
/* Register a callback function to be called for tab-completion. */
void Linenoise::SetCompletionCallback(linenoiseCompletionCallback *fn) {
completionCallback = fn;
}
/* Register a hits function to be called to show hits to the user at the
* right of the prompt. */
void Linenoise::SetHintsCallback(linenoiseHintsCallback *fn) {
hintsCallback = fn;
}
/* Register a function to free the hints returned by the hints callback
* registered with linenoiseSetHintsCallback(). */
void Linenoise::SetFreeHintsCallback(linenoiseFreeHintsCallback *fn) {
freeHintsCallback = fn;
}
linenoiseHintsCallback *Linenoise::HintsCallback() {
return hintsCallback;
}
linenoiseFreeHintsCallback *Linenoise::FreeHintsCallback() {
return freeHintsCallback;
}
TabCompletion Linenoise::TabComplete() const {
TabCompletion result;
if (!completionCallback) {
return result;
}
linenoiseCompletions lc;
lc.cvec = nullptr;
lc.len = 0;
// complete based on the cursor position
auto prev_char = buf[pos];
buf[pos] = '\0';
completionCallback(buf, &lc);
buf[pos] = prev_char;
result.completions.reserve(lc.len);
for (idx_t i = 0; i < lc.len; i++) {
Completion c;
c.completion = lc.cvec[i];
c.cursor_pos = c.completion.size();
c.completion += buf + pos;
result.completions.emplace_back(std::move(c));
}
freeCompletions(&lc);
return result;
}
/* This is an helper function for linenoiseEdit() and is called when the
* user types the <tab> key in order to complete the string currently in the
* input.
*
* The state of the editing is encapsulated into the pointed linenoiseState
* structure as described in the structure definition. */
int Linenoise::CompleteLine(EscapeSequence &current_sequence) {
int nread, nwritten;
char c = 0;
auto completion_list = TabComplete();
auto &completions = completion_list.completions;
if (completions.empty()) {
Terminal::Beep();
} else {
bool stop = false;
bool accept_completion = false;
idx_t i = 0;
while (!stop) {
/* Show completion or original buffer */
if (i < completions.size()) {
Linenoise saved = *this;
len = completions[i].completion.size();
pos = completions[i].cursor_pos;
buf = (char *)completions[i].completion.c_str();
RefreshLine();
len = saved.len;
pos = saved.pos;
buf = saved.buf;
} else {
RefreshLine();
}
nread = read(ifd, &c, 1);
if (nread <= 0) {
return -1;
}
Linenoise::Log("\nComplete Character %d\n", (int)c);
switch (c) {
case TAB: /* tab */
i = (i + 1) % completions.size();
break;
case ESC: { /* escape */
auto escape = Terminal::ReadEscapeSequence(ifd);
switch (escape) {
case EscapeSequence::SHIFT_TAB:
// shift-tab: move backwards
if (i == 0) {
// pressing shift-tab at the first completion cancels completion
RefreshLine();
current_sequence = escape;
stop = true;
} else {
i--;
}
break;
case EscapeSequence::ESCAPE:
/* Re-show original buffer */
RefreshLine();
current_sequence = escape;
stop = true;
break;
default:
current_sequence = escape;
accept_completion = true;
stop = true;
break;
}
break;
}
default:
accept_completion = true;
stop = true;
break;
}
if (stop && accept_completion && i < completions.size()) {
/* Update buffer and return */
if (i < completions.size()) {
nwritten = snprintf(buf, buflen, "%s", completions[i].completion.c_str());
pos = completions[i].cursor_pos;
len = nwritten;
}
}
}
}
return c; /* Return last read character */
}
size_t Linenoise::ComputeRenderWidth(const char *buf, size_t len) {
// utf8 in prompt, get render width
size_t cpos = 0;
size_t render_width = 0;
int sz;
while (cpos < len) {
if (duckdb::Utf8Proc::UTF8ToCodepoint(buf + cpos, sz) < 0) {
cpos++;
render_width++;
continue;
}
size_t char_render_width = duckdb::Utf8Proc::RenderWidth(buf, len, cpos);
cpos = duckdb::Utf8Proc::NextGraphemeCluster(buf, len, cpos);
render_width += char_render_width;
}
return render_width;
}
int Linenoise::GetPromptWidth() const {
return int(ComputeRenderWidth(prompt, strlen(prompt)));
}
int Linenoise::GetRenderPosition(const char *buf, size_t len, int max_width, int *n) {
if (duckdb::Utf8Proc::IsValid(buf, len)) {
// utf8 in prompt, get render width
size_t cpos = 0;
size_t render_width = 0;
while (cpos < len) {
size_t char_render_width = duckdb::Utf8Proc::RenderWidth(buf, len, cpos);
if (int(render_width + char_render_width) > max_width) {
*n = render_width;
return cpos;
}
cpos = duckdb::Utf8Proc::NextGraphemeCluster(buf, len, cpos);
render_width += char_render_width;
}
*n = render_width;
return len;
} else {
// invalid utf8, return -1
return -1;
}
}
int Linenoise::ParseOption(const char **azArg, int nArg, const char **out_error) {
if (strcmp(azArg[0], "highlight") == 0) {
if (nArg == 2) {
if (strcmp(azArg[1], "off") == 0 || strcmp(azArg[1], "0") == 0) {
Highlighting::Disable();
return 1;
} else if (strcmp(azArg[1], "on") == 0 || strcmp(azArg[1], "1") == 0) {
Highlighting::Enable();
return 1;
}
}
*out_error = "Expected usage: .highlight [off|on]";
return 1;
} else if (strcmp(azArg[0], "render_errors") == 0) {
if (nArg == 2) {
if (strcmp(azArg[1], "off") == 0 || strcmp(azArg[1], "0") == 0) {
Linenoise::DisableErrorRendering();
return 1;
} else if (strcmp(azArg[1], "on") == 0 || strcmp(azArg[1], "1") == 0) {
Linenoise::EnableErrorRendering();
return 1;
}
}
*out_error = "Expected usage: .render_errors [off|on]";
return 1;
} else if (strcmp(azArg[0], "render_completion") == 0) {
if (nArg == 2) {
if (strcmp(azArg[1], "off") == 0 || strcmp(azArg[1], "0") == 0) {
Linenoise::DisableCompletionRendering();
return 1;
} else if (strcmp(azArg[1], "on") == 0 || strcmp(azArg[1], "1") == 0) {
Linenoise::EnableCompletionRendering();
return 1;
}
}
*out_error = "Expected usage: .render_completion [off|on]";
return 1;
} else if (strcmp(azArg[0], "keyword") == 0 || strcmp(azArg[0], "constant") == 0 ||
strcmp(azArg[0], "comment") == 0 || strcmp(azArg[0], "error") == 0 || strcmp(azArg[0], "cont") == 0 ||
strcmp(azArg[0], "cont_sel") == 0) {
if (nArg == 2) {
const char *option = Highlighting::GetColorOption(azArg[1]);
if (option) {
HighlightingType type;
if (strcmp(azArg[0], "keyword") == 0) {
type = HighlightingType::KEYWORD;
} else if (strcmp(azArg[0], "constant") == 0) {
type = HighlightingType::CONSTANT;
} else if (strcmp(azArg[0], "comment") == 0) {
type = HighlightingType::COMMENT;
} else if (strcmp(azArg[0], "error") == 0) {
type = HighlightingType::ERROR;
} else if (strcmp(azArg[0], "cont") == 0) {
type = HighlightingType::CONTINUATION;
} else {
type = HighlightingType::CONTINUATION_SELECTED;
}
Highlighting::SetHighlightingColor(type, option);
return 1;
}
}
*out_error = "Expected usage: .[keyword|constant|comment|error|cont|cont_sel] "
"[red|green|yellow|blue|magenta|cyan|white|brightblack|brightred|brightgreen|brightyellow|"
"brightblue|brightmagenta|brightcyan|brightwhite]";
return 1;
} else if (strcmp(azArg[0], "keywordcode") == 0 || strcmp(azArg[0], "constantcode") == 0 ||
strcmp(azArg[0], "commentcode") == 0 || strcmp(azArg[0], "errorcode") == 0 ||
strcmp(azArg[0], "contcode") == 0 || strcmp(azArg[0], "cont_selcode") == 0) {
if (nArg == 2) {
HighlightingType type;
if (strcmp(azArg[0], "keywordcode") == 0) {
type = HighlightingType::KEYWORD;
} else if (strcmp(azArg[0], "constantcode") == 0) {
type = HighlightingType::CONSTANT;
} else if (strcmp(azArg[0], "commentcode") == 0) {
type = HighlightingType::COMMENT;
} else if (strcmp(azArg[0], "errorcode") == 0) {
type = HighlightingType::ERROR;
} else if (strcmp(azArg[0], "contcode") == 0) {
type = HighlightingType::CONTINUATION;
} else {
type = HighlightingType::CONTINUATION_SELECTED;
}
Highlighting::SetHighlightingColor(type, azArg[1]);
return 1;
}
*out_error =
"Expected usage: .[keywordcode|constantcode|commentcode|errorcode|contcode|cont_selcode] [terminal_code]";
return 1;
} else if (strcmp(azArg[0], "multiline") == 0) {
linenoiseSetMultiLine(1);
return 1;
} else if (strcmp(azArg[0], "singleline") == 0) {
linenoiseSetMultiLine(0);
return 1;
}
return 0;
}
bool Linenoise::IsNewline(char c) {
return c == '\r' || c == '\n';
}
void Linenoise::NextPosition(const char *buf, size_t len, size_t &cpos, int &rows, int &cols, int plen) const {
if (IsNewline(buf[cpos])) {
// explicit newline! move to next line and insert a prompt
rows++;
cols = plen;
cpos++;
if (buf[cpos - 1] == '\r' && cpos < len && buf[cpos] == '\n') {
cpos++;
}
return;
}
int sz;
int char_render_width;
if (duckdb::Utf8Proc::UTF8ToCodepoint(buf + cpos, sz) < 0) {
char_render_width = 1;
cpos++;
} else {
char_render_width = (int)duckdb::Utf8Proc::RenderWidth(buf, len, cpos);
cpos = duckdb::Utf8Proc::NextGraphemeCluster(buf, len, cpos);
}
if (cols + char_render_width > ws.ws_col) {
// exceeded l->cols, move to next row
rows++;
cols = char_render_width;
}
cols += char_render_width;
}
void Linenoise::PositionToColAndRow(size_t target_pos, int &out_row, int &out_col, int &rows, int &cols) const {
int plen = GetPromptWidth();
out_row = -1;
out_col = 0;
rows = 1;
cols = plen;
size_t cpos = 0;
while (cpos < len) {
if (cols >= ws.ws_col && !IsNewline(buf[cpos])) {
// exceeded width - move to next line
rows++;
cols = 0;
}
if (out_row < 0 && cpos >= target_pos) {
out_row = rows;
out_col = cols;
}
NextPosition(buf, len, cpos, rows, cols, plen);
}
if (target_pos == len) {
out_row = rows;
out_col = cols;
}
}
size_t Linenoise::ColAndRowToPosition(int target_row, int target_col) const {
int plen = GetPromptWidth();
int rows = 1;
int cols = plen;
size_t last_cpos = 0;
size_t cpos = 0;
while (cpos < len) {
if (cols >= ws.ws_col) {
// exceeded width - move to next line
rows++;
cols = 0;
}
if (rows > target_row) {
// we have skipped our target row - that means "target_col" was out of range for this row
// return the last position within the target row
return last_cpos;
}
if (rows == target_row) {
last_cpos = cpos;
}
if (rows == target_row && cols == target_col) {
return cpos;
}
NextPosition(buf, len, cpos, rows, cols, plen);
}
return cpos;
}
/* Insert the character 'c' at cursor current position.
*
* On error writing to the terminal -1 is returned, otherwise 0. */
void Linenoise::InsertCharacter(char c) {
if (len < buflen) {
if (len == pos) {
buf[pos] = c;
pos++;
len++;
buf[len] = '\0';
} else {
memmove(buf + pos + 1, buf + pos, len - pos);
buf[pos] = c;
len++;
pos++;
buf[len] = '\0';
}
}
}
int Linenoise::EditInsert(char c) {
if (has_more_data) {
render = false;
}
insert = true;
InsertCharacter(c);
RefreshLine();
return 0;
}
int Linenoise::EditInsertMulti(const char *c) {
for (size_t pos = 0; c[pos]; pos++) {
InsertCharacter(c[pos]);
}
RefreshLine();
return 0;
}
size_t Linenoise::PrevChar() const {
return duckdb::Utf8Proc::PreviousGraphemeCluster(buf, len, pos);
}
size_t Linenoise::NextChar() const {
return duckdb::Utf8Proc::NextGraphemeCluster(buf, len, pos);
}
/* Move cursor on the left. */
void Linenoise::EditMoveLeft() {
if (pos > 0) {
pos = PrevChar();
RefreshLine();
}
}
/* Move cursor on the right. */
void Linenoise::EditMoveRight() {
if (pos != len) {
pos = NextChar();
RefreshLine();
}
}
bool Linenoise::IsWordBoundary(char c) {
if (c >= 'a' && c <= 'z') {
return false;
}
if (c >= 'A' && c <= 'Z') {
return false;
}
if (c >= '0' && c <= '9') {
return false;
}
return true;
}
/* Move cursor to the next left word. */
void Linenoise::EditMoveWordLeft() {
if (pos == 0) {
return;
}
do {
pos = PrevChar();
} while (pos > 0 && !IsWordBoundary(buf[pos]));
RefreshLine();
}
/* Move cursor on the right. */
void Linenoise::EditMoveWordRight() {
if (pos == len) {
return;
}
do {
pos = NextChar();
} while (pos != len && !IsWordBoundary(buf[pos]));
RefreshLine();
}
/* Move cursor one row up. */
bool Linenoise::EditMoveRowUp() {
if (!Terminal::IsMultiline()) {
return false;
}
int rows, cols;
int cursor_row, cursor_col;
PositionToColAndRow(pos, cursor_row, cursor_col, rows, cols);
if (cursor_row <= 1) {
return false;
}
// we can move the cursor a line up
Linenoise::Log("source pos %d", pos);
Linenoise::Log("move from row %d to row %d", cursor_row, cursor_row - 1);
cursor_row--;
pos = ColAndRowToPosition(cursor_row, cursor_col);
Linenoise::Log("new pos %d", pos);
RefreshLine();
return true;
}
/* Move cursor one row down. */
bool Linenoise::EditMoveRowDown() {
if (!Terminal::IsMultiline()) {
return false;
}
int rows, cols;
int cursor_row, cursor_col;
PositionToColAndRow(pos, cursor_row, cursor_col, rows, cols);
if (cursor_row >= rows) {
return false;
}
// we can move the cursor a line down
Linenoise::Log("source pos %d", pos);
Linenoise::Log("move from row %d to row %d", cursor_row, cursor_row + 1);
cursor_row++;
pos = ColAndRowToPosition(cursor_row, cursor_col);
Linenoise::Log("new pos %d", pos);
RefreshLine();
return true;
}
/* Move cursor to the start of the buffer. */
void Linenoise::EditMoveHome() {
if (pos != 0) {
pos = 0;
RefreshLine();
}
}
/* Move cursor to the end of the buffer. */
void Linenoise::EditMoveEnd() {
if (pos != len) {
pos = len;
RefreshLine();
}
}
/* Move cursor to the start of the line. */
void Linenoise::EditMoveStartOfLine() {
while (pos > 0 && buf[pos - 1] != '\n') {
pos--;
}
RefreshLine();
}
/* Move cursor to the end of the line. */
void Linenoise::EditMoveEndOfLine() {
while (pos < len && buf[pos + 1] != '\n') {
pos++;
}
RefreshLine();
}
/* Substitute the currently edited line with the next or previous history
* entry as specified by 'dir'. */
void Linenoise::EditHistoryNext(HistoryScrollDirection dir) {
auto history_len = History::GetLength();
if (history_len > 1) {
/* Update the current history entry before to
* overwrite it with the next one. */
History::Overwrite(history_len - 1 - history_index, buf);
/* Show the new entry */
switch (dir) {
case HistoryScrollDirection::LINENOISE_HISTORY_PREV:
// scroll back
history_index++;
if (history_index >= history_len) {
history_index = history_len - 1;
return;
}
break;
case HistoryScrollDirection::LINENOISE_HISTORY_NEXT:
// scroll forwards
if (history_index == 0) {
return;
}
history_index--;
break;
case HistoryScrollDirection::LINENOISE_HISTORY_END:
history_index = 0;
break;
case HistoryScrollDirection::LINENOISE_HISTORY_START:
history_index = history_len - 1;
break;
}
strncpy(buf, History::GetEntry(history_len - 1 - history_index), buflen);
buf[buflen - 1] = '\0';
len = pos = strlen(buf);
if (Terminal::IsMultiline() && dir == HistoryScrollDirection::LINENOISE_HISTORY_NEXT) {
pos = ColAndRowToPosition(1, len);
}
RefreshLine();
}
}
/* Delete the character at the right of the cursor without altering the cursor
* position. Basically this is what happens with the "Delete" keyboard key. */
void Linenoise::EditDelete() {
if (len > 0 && pos < len) {
size_t new_pos = NextChar();
size_t char_sz = new_pos - pos;
memmove(buf + pos, buf + new_pos, len - new_pos);
len -= char_sz;
buf[len] = '\0';
RefreshLine();
}
}
/* Backspace implementation. */
void Linenoise::EditBackspace() {
if (pos > 0 && len > 0) {
size_t new_pos = PrevChar();
size_t char_sz = pos - new_pos;
memmove(buf + new_pos, buf + pos, len - pos);
len -= char_sz;
pos = new_pos;
buf[len] = '\0';
RefreshLine();
}
}
bool Linenoise::IsSpace(char c) {
switch (c) {
case ' ':
case '\r':
case '\n':
case '\t':
return true;
default:
return false;
}
}
/* Delete the previous word, maintaining the cursor at the start of the
* current word. */
void Linenoise::EditDeletePrevWord() {
size_t old_pos = pos;
size_t diff;
while (pos > 0 && IsSpace(buf[pos - 1])) {
pos--;
}
while (pos > 0 && !IsSpace(buf[pos - 1])) {
pos--;
}
diff = old_pos - pos;
memmove(buf + pos, buf + old_pos, len - old_pos + 1);
len -= diff;
RefreshLine();
}
/* Delete the next word */
void Linenoise::EditDeleteNextWord() {
size_t next_pos = pos;
size_t diff;
while (next_pos < len && IsSpace(buf[next_pos])) {
next_pos++;
}
while (next_pos < len && !IsSpace(buf[next_pos])) {
next_pos++;
}
diff = next_pos - pos;
memmove(buf + pos, buf + next_pos, len - next_pos + 1);
len -= diff;
RefreshLine();
}
void Linenoise::EditRemoveSpaces() {
size_t start_pos = pos;
size_t end_pos = pos;
size_t diff;
while (start_pos > 0 && buf[start_pos - 1] == ' ') {
start_pos--;
}
while (end_pos < len && buf[end_pos] == ' ') {
end_pos++;
}
diff = end_pos - start_pos;
memmove(buf + start_pos, buf + end_pos, len - end_pos + 1);
len -= diff;
pos = start_pos;
RefreshLine();
}
void Linenoise::EditSwapCharacter() {
if (pos == 0 || len < 2) {
return;
}
char temp_buffer[128];
if (pos + 1 > len) {
pos = PrevChar();
}
int prev_pos = PrevChar();
int next_pos = NextChar();
int prev_char_size = pos - prev_pos;
int cur_char_size = next_pos - pos;
memcpy(temp_buffer, buf + prev_pos, prev_char_size);
memmove(buf + prev_pos, buf + pos, cur_char_size);
memcpy(buf + prev_pos + cur_char_size, temp_buffer, prev_char_size);
pos = next_pos;
RefreshLine();
}
void Linenoise::EditSwapWord() {
idx_t current_pos = pos;
idx_t left_word_start, left_word_end;
idx_t right_word_start, right_word_end;
// move to the end of the right word
idx_t end_pos = pos;
while (end_pos < len && IsSpace(buf[end_pos])) {
end_pos++;
}
while (end_pos < len && !IsSpace(buf[end_pos])) {
end_pos++;
}
current_pos = end_pos;
while (current_pos > 0 && IsSpace(buf[current_pos - 1])) {
current_pos--;
}
right_word_end = current_pos;
// move to the start of the right word
while (current_pos > 0 && !IsSpace(buf[current_pos - 1])) {
current_pos--;
}
right_word_start = current_pos;
// move to the left of the left word
while (current_pos > 0 && IsSpace(buf[current_pos - 1])) {
current_pos--;
}
left_word_end = current_pos;
while (current_pos > 0 && !IsSpace(buf[current_pos - 1])) {
current_pos--;
}
left_word_start = current_pos;
if (left_word_start == right_word_start) {
// no words to swap
return;
}
idx_t left_len = left_word_end - left_word_start;
idx_t right_len = right_word_end - right_word_start;
if (left_len == 0 || right_len == 0) {
// one of the words has no length
return;
}
// now we need to swap [LEFT][RIGHT] to [RIGHT][LEFT]
// there's certainly some fancy way of doing this in-place
// but this is way easier
string left_word(buf + left_word_start, left_len);
string right_word(buf + right_word_start, right_len);
memset(buf + left_word_start, ' ', end_pos - left_word_start);
memcpy(buf + left_word_start, right_word.c_str(), right_word.size());
memcpy(buf + end_pos - left_len, left_word.c_str(), left_word.size());
pos = end_pos;
RefreshLine();
}
void Linenoise::EditDeleteAll() {
buf[0] = '\0';
pos = len = 0;
RefreshLine();
}
void Linenoise::EditCapitalizeNextWord(Capitalization capitalization) {
size_t next_pos = pos;
// find the next word
while (next_pos < len && IsSpace(buf[next_pos])) {
next_pos++;
}
bool first_char = true;
while (next_pos < len && !IsSpace(buf[next_pos])) {
switch (capitalization) {
case Capitalization::CAPITALIZE: {
if (first_char) {
first_char = false;
if (buf[next_pos] >= 'a' && buf[next_pos] <= 'z') {
buf[next_pos] -= 'a' - 'A';
}
}
break;
}
case Capitalization::LOWERCASE: {
if (buf[next_pos] >= 'A' && buf[next_pos] <= 'Z') {
buf[next_pos] += 'a' - 'A';
}
break;
}
case Capitalization::UPPERCASE: {
if (buf[next_pos] >= 'a' && buf[next_pos] <= 'z') {
buf[next_pos] -= 'a' - 'A';
}
break;
}
default:
break;
}
next_pos++;
}
pos = next_pos;
RefreshLine();
}
void Linenoise::StartSearch() {
// initiate reverse search
search = true;
search_buf = std::string();
search_matches.clear();
search_index = 0;
RefreshSearch();
}
void Linenoise::CancelSearch() {
search = false;
search_buf = std::string();
search_matches.clear();
search_index = 0;
RefreshLine();
}
char Linenoise::AcceptSearch(char nextCommand) {
if (search_index < search_matches.size()) {
// if there is a match - copy it into the buffer
auto match = search_matches[search_index];
auto history_entry = History::GetEntry(match.history_index);
auto history_len = strlen(history_entry);
memcpy(buf, history_entry, history_len);
buf[history_len] = '\0';
pos = match.match_end;
len = history_len;
}
CancelSearch();
return nextCommand;
}
void Linenoise::PerformSearch() {
// we try to maintain the current match while searching
size_t current_match = History::GetLength();
if (search_index < search_matches.size()) {
current_match = search_matches[search_index].history_index;
}
search_matches.clear();
search_index = 0;
if (search_buf.empty()) {
return;
}
std::unordered_set<std::string> matches;
auto lsearch = duckdb::StringUtil::Lower(search_buf);
for (size_t i = History::GetLength(); i > 0; i--) {
size_t history_index = i - 1;
auto lhistory = duckdb::StringUtil::Lower(History::GetEntry(history_index));
if (matches.find(lhistory) != matches.end()) {
continue;
}
matches.insert(lhistory);
auto entry = lhistory.find(lsearch);
if (entry != duckdb::string::npos) {
if (history_index == current_match) {
search_index = search_matches.size();
}
searchMatch match;
match.history_index = history_index;
match.match_start = entry;
match.match_end = entry + lsearch.size();
search_matches.push_back(match);
}
}
}
void Linenoise::SearchPrev() {
if (search_index > 0) {
search_index--;
} else if (search_matches.size() > 0) {
search_index = search_matches.size() - 1;
}
}
void Linenoise::SearchNext() {
search_index += 1;
if (search_index >= search_matches.size()) {
search_index = 0;
}
}
char Linenoise::Search(char c) {
switch (c) {
case CTRL_J:
case ENTER: /* enter */
// accept search and run
return AcceptSearch(ENTER);
case CTRL_R:
case CTRL_S:
// move to the next match index
SearchNext();
break;
case ESC: /* escape sequence */ {
auto escape = Terminal::ReadEscapeSequence(ifd);
switch (escape) {
case EscapeSequence::ESCAPE:
// double escape accepts search without any additional command
return AcceptSearch(0);
case EscapeSequence::UP:
SearchPrev();
break;
case EscapeSequence::DOWN:
SearchNext();
break;
case EscapeSequence::HOME:
return AcceptSearch(CTRL_A);
case EscapeSequence::END:
return AcceptSearch(CTRL_E);
case EscapeSequence::LEFT:
return AcceptSearch(CTRL_B);
case EscapeSequence::RIGHT:
return AcceptSearch(CTRL_F);
default:
break;
}
break;
}
case CTRL_A: // accept search, move to start of line
return AcceptSearch(CTRL_A);
case '\t':
case CTRL_E: // accept search - move to end of line
return AcceptSearch(CTRL_E);
case CTRL_B: // accept search - move cursor left
return AcceptSearch(CTRL_B);
case CTRL_F: // accept search - move cursor right
return AcceptSearch(CTRL_F);
case CTRL_T: // accept search: swap character
return AcceptSearch(CTRL_T);
case CTRL_U: // accept search, clear buffer
return AcceptSearch(CTRL_U);
case CTRL_K: // accept search, clear after cursor
return AcceptSearch(CTRL_K);
case CTRL_D: // accept search, delete a character
return AcceptSearch(CTRL_D);
case CTRL_L:
linenoiseClearScreen();
break;
case CTRL_P:
SearchPrev();
break;
case CTRL_N:
SearchNext();
break;
case CTRL_C:
case CTRL_G:
// abort search
CancelSearch();
return 0;
case BACKSPACE: /* backspace */
case CTRL_H: /* ctrl-h */
case CTRL_W: /* ctrl-w */
// remove trailing UTF-8 bytes (if any)
while (!search_buf.empty() && ((search_buf.back() & 0xc0) == 0x80)) {
search_buf.pop_back();
}
// finally remove the first UTF-8 byte
if (!search_buf.empty()) {
search_buf.pop_back();
}
PerformSearch();
break;
default:
// add input to search buffer
search_buf += c;
// perform the search
PerformSearch();
break;
}
RefreshSearch();
return 0;
}
bool Linenoise::AllWhitespace(const char *z) {
for (; *z; z++) {
if (StringUtil::CharacterIsSpace((unsigned char)z[0]))
continue;
if (*z == '/' && z[1] == '*') {
z += 2;
while (*z && (*z != '*' || z[1] != '/')) {
z++;
}
if (*z == 0) {
return false;
}
z++;
continue;
}
if (*z == '-' && z[1] == '-') {
z += 2;
while (*z && *z != '\n') {
z++;
}
if (*z == 0) {
return true;
}
continue;
}
return false;
}
return true;
}
Linenoise::Linenoise(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt)
: ifd(stdin_fd), ofd(stdout_fd), buf(buf), buflen(buflen), prompt(prompt), plen(strlen(prompt)) {
pos = 0;
old_cursor_rows = 1;
len = 0;
ws = Terminal::GetTerminalSize();
maxrows = 0;
history_index = 0;
y_scroll = 0;
clear_screen = false;
search = false;
has_more_data = false;
render = true;
continuation_markers = true;
insert = false;
search_index = 0;
/* Buffer starts empty. */
buf[0] = '\0';
buflen--; /* Make sure there is always space for the nulterm */
}
/* This function is the core of the line editing capability of linenoise.
* It expects 'fd' to be already in "raw mode" so that every key pressed
* will be returned ASAP to read().
*
* The resulting string is put into 'buf' when the user type enter, or
* when ctrl+d is typed.
*
* The function returns the length of the current buffer. */
int Linenoise::Edit() {
/* The latest history entry is always our current buffer, that
* initially is just an empty string. */
History::Add("");
if (write(ofd, prompt, plen) == -1) {
return -1;
}
while (true) {
EscapeSequence current_sequence = EscapeSequence::INVALID;
char c;
int nread;
nread = read(ifd, &c, 1);
if (nread <= 0) {
return len;
}
has_more_data = Terminal::HasMoreData(ifd);
render = true;
insert = false;
if (Terminal::IsMultiline() && !has_more_data) {
TerminalSize new_size = Terminal::GetTerminalSize();
if (new_size.ws_col != ws.ws_col || new_size.ws_row != ws.ws_row) {
// terminal resize! re-compute max lines
ws = new_size;
int rows, cols;
int cursor_row, cursor_col;
PositionToColAndRow(pos, cursor_row, cursor_col, rows, cols);
old_cursor_rows = cursor_row;
maxrows = rows;
}
}
if (search) {
char ret = Search(c);
if (search || ret == '\0') {
// still searching - continue searching
continue;
}
// run subsequent command
c = ret;
}
/* Only autocomplete when the callback is set. It returns < 0 when
* there was an error reading from fd. Otherwise it will return the
* character that should be handled next. */
if (c == TAB && completionCallback != NULL) {
if (has_more_data) {
// if there is more data, this tab character was added as part of copy-pasting data
// instead insert some spaces
if (EditInsertMulti(" ")) {
return -1;
}
continue;
}
c = CompleteLine(current_sequence);
/* Return on errors */
if (c < 0) {
return len;
}
/* Read next character when 0 */
if (c == 0) {
continue;
}
}
Linenoise::Log("%d\n", (int)c);
switch (c) {
case CTRL_G:
case CTRL_J:
case ENTER: { /* enter */
#ifdef LINENOISE_EDITOR
if (len > 0) {
// check if this contains ".edit"
// scroll back to last newline
idx_t begin_pos;
for (begin_pos = len; begin_pos > 0 && buf[begin_pos - 1] != '\n'; begin_pos--) {
}
// check if line is ".edit"
bool open_editor = false;
if (begin_pos + 5 == len && memcmp(buf + begin_pos, ".edit", 5) == 0) {
open_editor = true;
}
// check if line is "\\e"
if (begin_pos + 2 == len && memcmp(buf + begin_pos, "\\e", 2) == 0) {
open_editor = true;
}
if (open_editor) {
// .edit
// clear the buffer and open the editor
pos = len = begin_pos;
if (!EditBufferWithEditor(nullptr)) {
// failed to edit - refresh the removal of ".edit" / "\e"
RefreshLine();
break;
}
}
}
#endif
if (Terminal::IsMultiline() && len > 0) {
// check if this forms a complete SQL statement or not
buf[len] = '\0';
if (buf[0] != '.' && !AllWhitespace(buf) && !sqlite3_complete(buf)) {
// not a complete SQL statement yet! continuation
pos = len;
if (c != CTRL_G) {
// insert "\r\n" at the end if this is enter/ctrl+j
if (EditInsertMulti("\r\n")) {
return -1;
}
break;
} else {
// if this is Ctrl+G, terminate the statement
if (EditInsertMulti(";")) {
return -1;
}
}
}
}
// final refresh before returning control to the shell
continuation_markers = false;
History::RemoveLastEntry();
if (Terminal::IsMultiline()) {
if (pos == len) {
// already at the end - only refresh
RefreshLine();
} else {
EditMoveEnd();
}
}
if (hintsCallback) {
/* Force a refresh without hints to leave the previous
* line as the user typed it after a newline. */
linenoiseHintsCallback *hc = hintsCallback;
hintsCallback = NULL;
RefreshLine();
hintsCallback = hc;
}
// rewrite \r\n to \n
idx_t new_len = 0;
for (idx_t i = 0; i < len; i++) {
if (buf[i] == '\r' && buf[i + 1] == '\n') {
continue;
}
buf[new_len++] = buf[i];
}
buf[new_len] = '\0';
return (int)new_len;
}
case CTRL_O:
case CTRL_C: /* ctrl-c */ {
if (Terminal::IsMultiline()) {
continuation_markers = false;
// force a refresh by setting pos to 0
pos = 0;
EditMoveEnd();
}
buf[0] = '\3';
// we keep track of whether or not the line was empty by writing \3 to the second position of the line
// this is because at a higher level we might want to know if we pressed ctrl c to clear the line
// or to exit the process
if (len > 0) {
buf[1] = '\3';
buf[2] = '\0';
pos = 2;
len = 2;
} else {
buf[1] = '\0';
pos = 1;
len = 1;
}
return (int)len;
}
case BACKSPACE: /* backspace */
case CTRL_H: /* ctrl-h */
EditBackspace();
break;
case CTRL_D: /* ctrl-d, remove char at right of cursor, or if the
line is empty, act as end-of-file. */
if (len > 0) {
EditDelete();
} else {
History::RemoveLastEntry();
return -1;
}
break;
case CTRL_Z: /* ctrl-z, suspends shell */
Terminal::DisableRawMode();
raise(SIGTSTP);
Terminal::EnableRawMode();
RefreshLine();
break;
case CTRL_T: /* ctrl-t, swaps current character with previous. */
EditSwapCharacter();
break;
case CTRL_B: /* ctrl-b */
EditMoveLeft();
break;
case CTRL_F: /* ctrl-f */
EditMoveRight();
break;
case CTRL_P: /* ctrl-p */
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_PREV);
break;
case CTRL_N: /* ctrl-n */
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_NEXT);
break;
case CTRL_S:
case CTRL_R: /* ctrl-r */ {
StartSearch();
break;
}
case ESC: /* escape sequence */ {
EscapeSequence escape;
if (current_sequence == EscapeSequence::INVALID) {
// read escape sequence
escape = Terminal::ReadEscapeSequence(ifd);
} else {
// use stored sequence
escape = current_sequence;
current_sequence = EscapeSequence::INVALID;
}
switch (escape) {
case EscapeSequence::ALT_LEFT_ARROW:
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_START);
break;
case EscapeSequence::ALT_RIGHT_ARROW:
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_END);
break;
case EscapeSequence::CTRL_MOVE_BACKWARDS:
case EscapeSequence::ALT_B:
EditMoveWordLeft();
break;
case EscapeSequence::CTRL_MOVE_FORWARDS:
case EscapeSequence::ALT_F:
EditMoveWordRight();
break;
case EscapeSequence::ALT_D:
EditDeleteNextWord();
break;
case EscapeSequence::ALT_C:
EditCapitalizeNextWord(Capitalization::CAPITALIZE);
break;
case EscapeSequence::ALT_L:
EditCapitalizeNextWord(Capitalization::LOWERCASE);
break;
case EscapeSequence::ALT_N:
case EscapeSequence::ALT_P:
StartSearch();
break;
case EscapeSequence::ALT_T:
EditSwapWord();
break;
case EscapeSequence::ALT_U:
EditCapitalizeNextWord(Capitalization::UPPERCASE);
break;
case EscapeSequence::ALT_R:
EditDeleteAll();
break;
case EscapeSequence::ALT_BACKSPACE:
EditDeletePrevWord();
break;
case EscapeSequence::ALT_BACKSLASH:
EditRemoveSpaces();
break;
case EscapeSequence::HOME:
EditMoveHome();
break;
case EscapeSequence::END:
EditMoveEnd();
break;
case EscapeSequence::UP:
if (EditMoveRowUp()) {
break;
}
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_PREV);
break;
case EscapeSequence::DOWN:
if (EditMoveRowDown()) {
break;
}
EditHistoryNext(HistoryScrollDirection::LINENOISE_HISTORY_NEXT);
break;
case EscapeSequence::RIGHT:
EditMoveRight();
break;
case EscapeSequence::LEFT:
EditMoveLeft();
break;
case EscapeSequence::DELETE:
EditDelete();
break;
default:
Linenoise::Log("Unrecognized escape\n");
break;
}
break;
}
case CTRL_U: /* Ctrl+u, delete the whole line. */
EditDeleteAll();
break;
case CTRL_K: /* Ctrl+k, delete from current to end of line. */
buf[pos] = '\0';
len = pos;
RefreshLine();
break;
case CTRL_A: /* Ctrl+a, go to the start of the line */
EditMoveStartOfLine();
break;
case CTRL_E: /* ctrl+e, go to the end of the line */
EditMoveEndOfLine();
break;
case CTRL_L: /* ctrl+l, clear screen */
linenoiseClearScreen();
clear_screen = true;
RefreshLine();
break;
case CTRL_W: /* ctrl+w, delete previous word */
EditDeletePrevWord();
break;
case CTRL_X: /* ctrl+x, insert newline */
EditInsertMulti("\r\n");
break;
case CTRL_Y:
// unsupported
break;
default: {
if (EditInsert(c)) {
return -1;
}
break;
}
}
}
return len;
}
#ifdef LINENOISE_LOGGING
void Linenoise::LogMessageRecursive(const string &msg, std::vector<ExceptionFormatValue> &values) {
static FILE *lndebug_fp = NULL;
if (!lndebug_fp) {
lndebug_fp = fopen("/tmp/lndebug.txt", "a");
}
auto log_message = Exception::ConstructMessageRecursive(msg, values);
fprintf(lndebug_fp, "%s", log_message.c_str());
fflush(lndebug_fp);
}
#endif
void Linenoise::LogTokens(const vector<highlightToken> &tokens) {
#ifdef LINENOISE_LOGGING
for (auto &token : tokens) {
const char *token_type_name = "";
switch (token.type) {
case tokenType::TOKEN_IDENTIFIER:
token_type_name = "IDENTIFIER";
break;
case tokenType::TOKEN_NUMERIC_CONSTANT:
token_type_name = "NUMERIC_CONSTANT";
break;
case tokenType::TOKEN_STRING_CONSTANT:
token_type_name = "STRING_CONSTANT";
break;
case tokenType::TOKEN_OPERATOR:
token_type_name = "OPERATOR";
break;
case tokenType::TOKEN_KEYWORD:
token_type_name = "KEYWORD";
break;
case tokenType::TOKEN_COMMENT:
token_type_name = "COMMENT";
break;
case tokenType::TOKEN_CONTINUATION:
token_type_name = "CONTINUATION";
break;
case tokenType::TOKEN_CONTINUATION_SELECTED:
token_type_name = "CONTINUATION_SELECTED";
break;
case tokenType::TOKEN_BRACKET:
token_type_name = "BRACKET";
break;
case tokenType::TOKEN_ERROR:
token_type_name = "ERROR";
break;
default:
break;
}
Linenoise::Log("Token at position %d with type %s\n", token.start, token_type_name);
}
#endif
}
#ifdef LINENOISE_EDITOR
// .edit functionality - code adopted from psql
bool Linenoise::EditFileWithEditor(const string &file_name, const char *editor) {
/* Find an editor to use */
if (!editor) {
editor = getenv("DUCKDB_EDITOR");
}
if (!editor) {
editor = getenv("EDITOR");
}
if (!editor) {
editor = getenv("VISUAL");
}
if (!editor) {
editor = DEFAULT_EDITOR;
}
/*
* On Unix the EDITOR value should *not* be quoted, since it might include
* switches, eg, EDITOR="pico -t"; it's up to the user to put quotes in it
* if necessary. But this policy is not very workable on Windows, due to
* severe brain damage in their command shell plus the fact that standard
* program paths include spaces.
*/
string command;
#ifndef WIN32
command = "exec " + string(editor) + " '" + file_name + "'";
#else
command = "\"" + string(editor) + "\" \"" + file_name + "\"";
#endif
int result = system(command.c_str());
if (result == -1) {
Log("could not start editor \"%s\"\n", editor);
} else if (result == 127) {
Log("could not start /bin/sh\n");
}
return result == 0;
}
bool Linenoise::EditBufferWithEditor(const char *editor) {
/* make a temp file to edit */
#ifndef WIN32
const char *tmpdir = getenv("TMPDIR");
if (!tmpdir) {
tmpdir = "/tmp";
}
#else
char tmpdir[MAX_PATH_LENGTH];
int ret;
ret = GetTempPath(MAX_PATH_LENGTH, tmpdir);
if (ret == 0 || ret > MAX_PATH_LENGTH) {
Log("cannot locate temporary directory: %s", !ret ? strerror(errno) : "");
return false;
}
#endif
string temporary_file_name;
#ifndef WIN32
temporary_file_name = string(tmpdir) + "/duckdb.edit." + std::to_string(getpid()) + ".sql";
#else
temporary_file_name = string(tmpdir) + "duckdb.edit." + std::to_string(getpid()) + ".sql";
#endif
FILE *f = fopen(temporary_file_name.c_str(), "w+");
if (!f) {
Log("could not open temporary file \"%s\": %s\n", temporary_file_name, strerror(errno));
Terminal::Beep();
return false;
}
// edit the current buffer by default
const char *write_buffer = buf;
idx_t write_len = len;
if (write_len == 0) {
// if the current buffer is empty we are typing ".edit" as the first command
// edit the previous history entry
auto edit_index = History::GetLength();
if (edit_index >= 2) {
auto history_entry = History::GetEntry(edit_index - 2);
if (history_entry) {
write_buffer = history_entry;
write_len = strlen(history_entry);
}
}
}
// write existing buffer to file
if (fwrite(write_buffer, 1, write_len, f) != write_len) {
Log("Failed to write data %s: %s\n", temporary_file_name, strerror(errno));
fclose(f);
remove(temporary_file_name.c_str());
Terminal::Beep();
return false;
}
fclose(f);
/* call editor */
if (!EditFileWithEditor(temporary_file_name, editor)) {
Terminal::Beep();
return false;
}
// read the file contents again
f = fopen(temporary_file_name.c_str(), "rb");
if (!f) {
Log("Failed to open file%s: %s\n", temporary_file_name, strerror(errno));
remove(temporary_file_name.c_str());
Terminal::Beep();
return false;
}
/* read file back into buffer */
string new_buffer;
char line[1024];
while (fgets(line, sizeof(line), f)) {
// strip the existing newline from the line obtained from fgets
// the reason for that is that we need the line endings to be "\r\n" for rendering purposes
idx_t line_len = strlen(line);
idx_t orig_len = line_len;
while (line_len > 0 && (line[line_len - 1] == '\r' || line[line_len - 1] == '\n')) {
line_len--;
}
new_buffer.append(line, line_len);
if (orig_len != line_len) {
// we stripped a newline - add a new newline (but this time always \r\n)
new_buffer += "\r\n";
}
}
if (ferror(f)) {
Log("Failed while reading back buffer %s: %s\n", temporary_file_name, strerror(errno));
Terminal::Beep();
}
fclose(f);
/* remove temp file */
if (remove(temporary_file_name.c_str()) == -1) {
Log("Failed to remove file \"%s\": %s\n", temporary_file_name, strerror(errno));
Terminal::Beep();
return false;
}
// copy back into buffer
memcpy(buf, new_buffer.c_str(), new_buffer.size());
len = new_buffer.size();
pos = len;
RefreshLine();
return true;
}
#endif
} // namespace duckdb