#include #include #include #include #include #include "linenoise.h" #include "linenoise.hpp" #include "history.hpp" #include "highlighting.hpp" #include "terminal.hpp" #include "utf8proc_wrapper.hpp" #include #include #include "duckdb_shell_wrapper.h" #include #include "sqlite3.h" #include "duckdb/common/string_util.hpp" #ifdef __MVS__ #include #include #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 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 ¤t_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 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 &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 &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