#include "linenoise.hpp" #include "highlighting.hpp" #include "history.hpp" #include "utf8proc_wrapper.hpp" #include namespace duckdb { static const char *continuationPrompt = "> "; static const char *continuationSelectedPrompt = "> "; static bool enableCompletionRendering = false; static bool enableErrorRendering = true; void Linenoise::EnableCompletionRendering() { enableCompletionRendering = true; } void Linenoise::DisableCompletionRendering() { enableCompletionRendering = false; } void Linenoise::EnableErrorRendering() { enableErrorRendering = true; } void Linenoise::DisableErrorRendering() { enableErrorRendering = false; } /* =========================== Line editing ================================= */ /* We define a very simple "append buffer" structure, that is an heap * allocated string where we can append to. This is useful in order to * write all the escape sequences in a buffer and flush them to the standard * output in a single call, to avoid flickering effects. */ struct AppendBuffer { void Append(const char *s, idx_t len) { buffer.append(s, len); } void Append(const char *s) { buffer.append(s); } void Write(int fd) { if (write(fd, buffer.c_str(), buffer.size()) == -1) { /* Can't recover from write error. */ Linenoise::Log("%s", "Failed to write buffer\n"); } } private: std::string buffer; }; void Linenoise::SetPrompt(const char *continuation, const char *continuationSelected) { continuationPrompt = continuation; continuationSelectedPrompt = continuationSelected; } /* Helper of refreshSingleLine() and refreshMultiLine() to show hints * to the right of the prompt. */ void Linenoise::RefreshShowHints(AppendBuffer &append_buffer, int plen) const { char seq[64]; auto hints_callback = Linenoise::HintsCallback(); if (hints_callback && plen + len < size_t(ws.ws_col)) { int color = -1, bold = 0; char *hint = hints_callback(buf, &color, &bold); if (hint) { int hintlen = strlen(hint); int hintmaxlen = ws.ws_col - (plen + len); if (hintlen > hintmaxlen) { hintlen = hintmaxlen; } if (bold == 1 && color == -1) color = 37; if (color != -1 || bold != 0) { snprintf(seq, 64, "\033[%d;%d;49m", bold, color); } else { seq[0] = '\0'; } append_buffer.Append(seq, strlen(seq)); append_buffer.Append(hint, hintlen); if (color != -1 || bold != 0) { append_buffer.Append("\033[0m"); } /* Call the function to free the hint returned. */ auto free_hints_callback = Linenoise::FreeHintsCallback(); if (free_hints_callback) { free_hints_callback(hint); } } } } static void renderText(size_t &render_pos, char *&buf, size_t &len, size_t pos, size_t cols, size_t plen, std::string &highlight_buffer, bool highlight, searchMatch *match = nullptr) { if (duckdb::Utf8Proc::IsValid(buf, len)) { // utf8 in prompt, handle rendering size_t remaining_render_width = cols - plen - 1; size_t start_pos = 0; size_t cpos = 0; size_t prev_pos = 0; size_t total_render_width = 0; while (cpos < len) { size_t char_render_width = duckdb::Utf8Proc::RenderWidth(buf, len, cpos); prev_pos = cpos; cpos = duckdb::Utf8Proc::NextGraphemeCluster(buf, len, cpos); total_render_width += cpos - prev_pos; if (total_render_width >= remaining_render_width) { // character does not fit anymore! we need to figure something out if (prev_pos >= pos) { // we passed the cursor: break cpos = prev_pos; break; } else { // we did not pass the cursor yet! remove characters from the start until it fits again while (total_render_width >= remaining_render_width) { size_t start_char_width = duckdb::Utf8Proc::RenderWidth(buf, len, start_pos); size_t new_start = duckdb::Utf8Proc::NextGraphemeCluster(buf, len, start_pos); total_render_width -= new_start - start_pos; start_pos = new_start; render_pos -= start_char_width; } } } if (prev_pos < pos) { render_pos += char_render_width; } } if (highlight) { bool is_dot_command = buf[0] == '.'; auto tokens = Highlighting::Tokenize(buf, len, is_dot_command, match); highlight_buffer = Highlighting::HighlightText(buf, len, start_pos, cpos, tokens); buf = (char *)highlight_buffer.c_str(); len = highlight_buffer.size(); } else { buf = buf + start_pos; len = cpos - start_pos; } } else { // invalid UTF8: fallback while ((plen + pos) >= cols) { buf++; len--; pos--; } while (plen + len > cols) { len--; } render_pos = pos; } } /* Single line low level line refresh. * * Rewrite the currently edited line accordingly to the buffer content, * cursor position, and number of columns of the terminal. */ void Linenoise::RefreshSingleLine() const { char seq[64]; size_t plen = GetPromptWidth(); int fd = ofd; char *render_buf = buf; size_t render_len = len; size_t render_pos = 0; std::string highlight_buffer; renderText(render_pos, render_buf, render_len, pos, ws.ws_col, plen, highlight_buffer, Highlighting::IsEnabled()); AppendBuffer append_buffer; /* Cursor to left edge */ append_buffer.Append("\r"); /* Write the prompt and the current buffer content */ append_buffer.Append(prompt); append_buffer.Append(render_buf, render_len); /* Show hits if any. */ RefreshShowHints(append_buffer, plen); /* Erase to right */ append_buffer.Append("\x1b[0K"); /* Move cursor to original position. */ snprintf(seq, 64, "\r\x1b[%dC", (int)(render_pos + plen)); append_buffer.Append(seq); append_buffer.Write(fd); } void Linenoise::RefreshSearch() { std::string search_prompt; static const size_t SEARCH_PROMPT_RENDER_SIZE = 28; std::string no_matches_text = "(no matches)"; bool no_matches = search_index >= search_matches.size(); if (search_buf.empty()) { search_prompt = "search" + std::string(SEARCH_PROMPT_RENDER_SIZE - 8, ' ') + "> "; no_matches_text = "(type to search)"; } else { std::string search_text; std::string matches_text; search_text += search_buf; if (!no_matches) { matches_text += std::to_string(search_index + 1); matches_text += "/" + std::to_string(search_matches.size()); } size_t search_text_length = ComputeRenderWidth(search_text.c_str(), search_text.size()); size_t matches_text_length = ComputeRenderWidth(matches_text.c_str(), matches_text.size()); size_t total_text_length = search_text_length + matches_text_length; if (total_text_length < SEARCH_PROMPT_RENDER_SIZE - 2) { // search text is short: we can render the entire search text search_prompt = search_text; search_prompt += std::string(SEARCH_PROMPT_RENDER_SIZE - 2 - total_text_length, ' '); search_prompt += matches_text; search_prompt += "> "; } else { // search text length is too long to fit: truncate bool render_matches = matches_text_length < SEARCH_PROMPT_RENDER_SIZE - 8; char *search_buf = (char *)search_text.c_str(); size_t search_len = search_text.size(); size_t search_render_pos = 0; size_t max_render_size = SEARCH_PROMPT_RENDER_SIZE - 3; if (render_matches) { max_render_size -= matches_text_length; } std::string highlight_buffer; renderText(search_render_pos, search_buf, search_len, search_len, max_render_size, 0, highlight_buffer, false); search_prompt = std::string(search_buf, search_len); for (size_t i = search_render_pos; i < max_render_size; i++) { search_prompt += " "; } if (render_matches) { search_prompt += matches_text; } search_prompt += "> "; } } auto oldHighlighting = Highlighting::IsEnabled(); Linenoise clone = *this; prompt = search_prompt.c_str(); plen = search_prompt.size(); if (no_matches || search_buf.empty()) { // if there are no matches render the no_matches_text buf = (char *)no_matches_text.c_str(); len = no_matches_text.size(); pos = 0; // don't highlight the "no_matches" text Highlighting::Disable(); } else { // if there are matches render the current history item auto search_match = search_matches[search_index]; auto history_index = search_match.history_index; auto cursor_position = search_match.match_end; buf = (char *)History::GetEntry(history_index); len = strlen(buf); pos = cursor_position; } RefreshLine(); if (oldHighlighting) { Highlighting::Enable(); } buf = clone.buf; len = clone.len; pos = clone.pos; prompt = clone.prompt; plen = clone.plen; } string Linenoise::AddContinuationMarkers(const char *buf, size_t len, int plen, int cursor_row, vector &tokens) const { std::string result; int rows = 1; int cols = plen; size_t cpos = 0; size_t prev_pos = 0; size_t extra_bytes = 0; // extra bytes introduced size_t token_position = 0; // token position vector new_tokens; new_tokens.reserve(tokens.size()); while (cpos < len) { bool is_newline = IsNewline(buf[cpos]); NextPosition(buf, len, cpos, rows, cols, plen); for (; prev_pos < cpos; prev_pos++) { result += buf[prev_pos]; } if (is_newline) { bool is_cursor_row = rows == cursor_row; const char *prompt = is_cursor_row ? continuationSelectedPrompt : continuationPrompt; if (!continuation_markers) { prompt = ""; } size_t continuationLen = strlen(prompt); size_t continuationRender = ComputeRenderWidth(prompt, continuationLen); // pad with spaces prior to prompt for (int i = int(continuationRender); i < plen; i++) { result += " "; } result += prompt; size_t continuationBytes = plen - continuationRender + continuationLen; if (token_position < tokens.size()) { for (; token_position < tokens.size(); token_position++) { if (tokens[token_position].start >= cpos) { // not there yet break; } tokens[token_position].start += extra_bytes; new_tokens.push_back(tokens[token_position]); } tokenType prev_type = tokenType::TOKEN_IDENTIFIER; if (token_position > 0 && token_position < tokens.size() + 1) { prev_type = tokens[token_position - 1].type; } highlightToken token; token.start = cpos + extra_bytes; token.type = is_cursor_row ? tokenType::TOKEN_CONTINUATION_SELECTED : tokenType::TOKEN_CONTINUATION; token.search_match = false; new_tokens.push_back(token); token.start = cpos + extra_bytes + continuationBytes; token.type = prev_type; token.search_match = false; new_tokens.push_back(token); } extra_bytes += continuationBytes; } } for (; prev_pos < cpos; prev_pos++) { result += buf[prev_pos]; } for (; token_position < tokens.size(); token_position++) { tokens[token_position].start += extra_bytes; new_tokens.push_back(tokens[token_position]); } tokens = std::move(new_tokens); return result; } // insert a token of length 1 of the specified type static void InsertToken(tokenType insert_type, idx_t insert_pos, vector &tokens) { vector new_tokens; new_tokens.reserve(tokens.size() + 1); idx_t i; bool found = false; for (i = 0; i < tokens.size(); i++) { // find the exact position where we need to insert the token if (tokens[i].start == insert_pos) { // this token is exactly at this render position // insert highlighting for the bracket highlightToken token; token.start = insert_pos; token.type = insert_type; token.search_match = false; new_tokens.push_back(token); // now we need to insert the other token ONLY if the other token is not immediately following this one if (i + 1 >= tokens.size() || tokens[i + 1].start > insert_pos + 1) { token.start = insert_pos + 1; token.type = tokens[i].type; token.search_match = false; new_tokens.push_back(token); } i++; found = true; break; } else if (tokens[i].start > insert_pos) { // the next token is AFTER the render position // insert highlighting for the bracket highlightToken token; token.start = insert_pos; token.type = insert_type; token.search_match = false; new_tokens.push_back(token); // now just insert the next token new_tokens.push_back(tokens[i]); i++; found = true; break; } else { // insert the token new_tokens.push_back(tokens[i]); } } // copy over the remaining tokens for (; i < tokens.size(); i++) { new_tokens.push_back(tokens[i]); } if (!found) { // token was not added - add it to the end highlightToken token; token.start = insert_pos; token.type = insert_type; token.search_match = false; new_tokens.push_back(token); } tokens = std::move(new_tokens); } enum class ScanState { STANDARD, IN_SINGLE_QUOTE, IN_DOUBLE_QUOTE, IN_COMMENT, DOLLAR_QUOTED_STRING }; static void OpenBracket(vector &brackets, vector &cursor_brackets, idx_t pos, idx_t i) { // check if the cursor is at this position if (pos == i) { // cursor is exactly on this position - always highlight this bracket if (!cursor_brackets.empty()) { cursor_brackets.clear(); } cursor_brackets.push_back(i); } if (cursor_brackets.empty() && ((i + 1) == pos || (pos + 1) == i)) { // cursor is either BEFORE or AFTER this bracket and we don't have any highlighted bracket yet // highlight this bracket cursor_brackets.push_back(i); } brackets.push_back(i); } static void CloseBracket(vector &brackets, vector &cursor_brackets, idx_t pos, idx_t i, vector &errors) { if (pos == i) { // cursor is on this closing bracket // clear any selected brackets - we always select this one cursor_brackets.clear(); } if (brackets.empty()) { // closing bracket without matching opening bracket errors.push_back(i); } else { if (cursor_brackets.size() == 1) { if (cursor_brackets.back() == brackets.back()) { // this closing bracket matches the highlighted opening cursor bracket - highlight both cursor_brackets.push_back(i); } } else if (cursor_brackets.empty() && (pos == i || (i + 1) == pos || (pos + 1) == i)) { // no cursor bracket selected yet and cursor is BEFORE or AFTER this bracket // add this bracket cursor_brackets.push_back(i); cursor_brackets.push_back(brackets.back()); } brackets.pop_back(); } } static void HandleBracketErrors(const vector &brackets, vector &errors) { if (brackets.empty()) { return; } // if there are unclosed brackets remaining not all brackets were closed for (auto &bracket : brackets) { errors.push_back(bracket); } } void Linenoise::AddErrorHighlighting(idx_t render_start, idx_t render_end, vector &tokens) const { static constexpr const idx_t MAX_ERROR_LENGTH = 2000; if (!enableErrorRendering) { return; } if (len >= MAX_ERROR_LENGTH) { return; } // do a pass over the buffer to collect errors: // * brackets without matching closing/opening bracket // * single quotes without matching closing single quote // * double quote without matching double quote ScanState state = ScanState::STANDARD; vector brackets; // () vector square_brackets; // [] vector curly_brackets; // {} vector errors; vector cursor_brackets; vector comment_start; vector comment_end; string dollar_quote_marker; idx_t quote_pos = 0; for (idx_t i = 0; i < len; i++) { auto c = buf[i]; switch (state) { case ScanState::STANDARD: switch (c) { case '-': if (i + 1 < len && buf[i + 1] == '-') { // -- puts us in a comment comment_start.push_back(i); i++; state = ScanState::IN_COMMENT; break; } break; case '\'': state = ScanState::IN_SINGLE_QUOTE; quote_pos = i; break; case '\"': state = ScanState::IN_DOUBLE_QUOTE; quote_pos = i; break; case '(': OpenBracket(brackets, cursor_brackets, pos, i); break; case '[': OpenBracket(square_brackets, cursor_brackets, pos, i); break; case '{': OpenBracket(curly_brackets, cursor_brackets, pos, i); break; case ')': CloseBracket(brackets, cursor_brackets, pos, i, errors); break; case ']': CloseBracket(square_brackets, cursor_brackets, pos, i, errors); break; case '}': CloseBracket(curly_brackets, cursor_brackets, pos, i, errors); break; case '$': { // dollar symbol if (i + 1 >= len) { // we need more than just a dollar break; } // check if this is a dollar-quoted string idx_t next_dollar = 0; for (idx_t idx = i + 1; idx < len; idx++) { if (buf[idx] == '$') { // found the next dollar next_dollar = idx; break; } // all characters can be between A-Z, a-z or \200 - \377 if (buf[idx] >= 'A' && buf[idx] <= 'Z') { continue; } if (buf[idx] >= 'a' && buf[idx] <= 'z') { continue; } if (buf[idx] >= '\200' && buf[idx] <= '\377') { continue; } // the first character CANNOT be a numeric, only subsequent characters if (idx > i + 1 && buf[idx] >= '0' && buf[idx] <= '9') { continue; } // not a dollar quoted string break; } if (next_dollar == 0) { // not a dollar quoted string break; } // dollar quoted string state = ScanState::DOLLAR_QUOTED_STRING; quote_pos = i; i = next_dollar; if (i < len) { // found a complete marker - store it idx_t marker_start = quote_pos + 1; dollar_quote_marker = string(buf + marker_start, i - marker_start); } break; } default: break; } break; case ScanState::IN_COMMENT: // comment state - the only thing that will get us out is a newline switch (c) { case '\r': case '\n': // newline - left comment state state = ScanState::STANDARD; comment_end.push_back(i); break; default: break; } break; case ScanState::IN_SINGLE_QUOTE: // single quote - all that will get us out is an unescaped single-quote if (c == '\'') { if (i + 1 < len && buf[i + 1] == '\'') { // double single-quote means the quote is escaped - continue i++; break; } else { // otherwise revert to standard scan state state = ScanState::STANDARD; break; } } break; case ScanState::IN_DOUBLE_QUOTE: // double quote - all that will get us out is an unescaped quote if (c == '"') { if (i + 1 < len && buf[i + 1] == '"') { // double quote means the quote is escaped - continue i++; break; } else { // otherwise revert to standard scan state state = ScanState::STANDARD; break; } } break; case ScanState::DOLLAR_QUOTED_STRING: { // dollar-quoted string - all that will get us out is a $[marker]$ if (c != '$') { break; } if (i + 1 >= len) { // no room for the final dollar break; } // skip to the next dollar symbol idx_t start = i + 1; idx_t end = start; while (end < len && buf[end] != '$') { end++; } if (end >= len) { // no final dollar found - continue as normal break; } if (end - start != dollar_quote_marker.size()) { // length mismatch - cannot match break; } if (memcmp(buf + start, dollar_quote_marker.c_str(), dollar_quote_marker.size()) != 0) { // marker mismatch break; } // marker found! revert to standard state dollar_quote_marker = string(); state = ScanState::STANDARD; i = end; break; } default: break; } } if (state == ScanState::IN_DOUBLE_QUOTE || state == ScanState::IN_SINGLE_QUOTE || state == ScanState::DOLLAR_QUOTED_STRING) { // quote is never closed errors.push_back(quote_pos); } HandleBracketErrors(brackets, errors); HandleBracketErrors(square_brackets, errors); HandleBracketErrors(curly_brackets, errors); // insert all the errors for highlighting for (auto &error : errors) { Linenoise::Log("Error found at position %llu\n", error); if (error < render_start || error > render_end) { continue; } auto render_error = error - render_start; InsertToken(tokenType::TOKEN_ERROR, render_error, tokens); } if (cursor_brackets.size() != 2) { // no matching cursor brackets found cursor_brackets.clear(); } // insert bracket for highlighting for (auto &bracket_position : cursor_brackets) { Linenoise::Log("Highlight bracket at position %d\n", bracket_position); if (bracket_position < render_start || bracket_position > render_end) { continue; } idx_t render_position = bracket_position - render_start; InsertToken(tokenType::TOKEN_BRACKET, render_position, tokens); } // insert comments if (!comment_start.empty()) { vector new_tokens; new_tokens.reserve(tokens.size()); idx_t token_idx = 0; for (idx_t c = 0; c < comment_start.size(); c++) { auto c_start = comment_start[c]; auto c_end = c < comment_end.size() ? comment_end[c] : len; if (c_start < render_start || c_end > render_end) { continue; } Linenoise::Log("Comment at position %d to %d\n", c_start, c_end); c_start -= render_start; c_end -= render_start; bool inserted_comment = false; highlightToken comment_token; comment_token.start = c_start; comment_token.type = tokenType::TOKEN_COMMENT; comment_token.search_match = false; for (; token_idx < tokens.size(); token_idx++) { if (tokens[token_idx].start >= c_start) { // insert the comment here new_tokens.push_back(comment_token); inserted_comment = true; break; } new_tokens.push_back(tokens[token_idx]); } if (!inserted_comment) { new_tokens.push_back(comment_token); } else { // skip all tokens until we exit the comment again for (; token_idx < tokens.size(); token_idx++) { if (tokens[token_idx].start > c_end) { break; } } } } for (; token_idx < tokens.size(); token_idx++) { new_tokens.push_back(tokens[token_idx]); } tokens = std::move(new_tokens); } } static bool IsCompletionCharacter(char c) { if (c >= 'A' && c <= 'Z') { return true; } if (c >= 'a' && c <= 'z') { return true; } if (c == '_') { return true; } return false; } bool Linenoise::AddCompletionMarker(const char *buf, idx_t len, string &result_buffer, vector &tokens) const { if (!enableCompletionRendering) { return false; } if (!continuation_markers) { // don't render when pressing ctrl+c, only when editing return false; } static constexpr const idx_t MAX_COMPLETION_LENGTH = 1000; if (len >= MAX_COMPLETION_LENGTH) { return false; } if (!insert || pos != len) { // only show when inserting a character at the end return false; } if (pos < 3) { // we need at least 3 bytes return false; } if (!tokens.empty() && tokens.back().type == tokenType::TOKEN_ERROR) { // don't show auto-completion when we have errors return false; } // we ONLY show completion if we have typed at least three characters that are supported for completion // for now this is ONLY the characters a-z, A-Z and underscore (_) for (idx_t i = pos - 3; i < pos; i++) { if (!IsCompletionCharacter(buf[i])) { return false; } } auto completion = TabComplete(); if (completion.completions.empty()) { // no completions found return false; } if (completion.completions[0].completion.size() <= len) { // completion is not long enough return false; } // we have stricter requirements for rendering completions - the completion must match exactly for (idx_t i = pos; i > 0; i--) { auto cpos = i - 1; if (!IsCompletionCharacter(buf[cpos])) { break; } if (completion.completions[0].completion[cpos] != buf[cpos]) { return false; } } // add the first completion found for rendering purposes result_buffer = string(buf, len); result_buffer += completion.completions[0].completion.substr(len); highlightToken completion_token; completion_token.start = len; completion_token.type = tokenType::TOKEN_COMMENT; completion_token.search_match = true; tokens.push_back(completion_token); return true; } /* Multi line low level line refresh. * * Rewrite the currently edited line accordingly to the buffer content, * cursor position, and number of columns of the terminal. */ void Linenoise::RefreshMultiLine() { if (!render) { return; } char seq[64]; int plen = GetPromptWidth(); // utf8 in prompt, get render width int rows, cols; int new_cursor_row, new_cursor_x; PositionToColAndRow(pos, new_cursor_row, new_cursor_x, rows, cols); int col; /* column position, zero-based. */ int old_rows = maxrows ? maxrows : 1; int fd = ofd; std::string highlight_buffer; auto render_buf = this->buf; auto render_len = this->len; idx_t render_start = 0; idx_t render_end = render_len; if (clear_screen) { old_cursor_rows = 0; old_rows = 0; clear_screen = false; } if (rows > ws.ws_row) { // the text does not fit in the terminal (too many rows) // enable scrolling mode // check if, given the current y_scroll, the cursor is visible // display range is [y_scroll, y_scroll + ws.ws_row] if (new_cursor_row < int(y_scroll) + 1) { y_scroll = new_cursor_row - 1; } else if (new_cursor_row > int(y_scroll) + int(ws.ws_row)) { y_scroll = new_cursor_row - ws.ws_row; } // display only characters up to the current scroll position if (y_scroll == 0) { render_start = 0; } else { render_start = ColAndRowToPosition(y_scroll, 0); } if (int(y_scroll) + int(ws.ws_row) >= rows) { render_end = len; } else { render_end = ColAndRowToPosition(y_scroll + ws.ws_row, 99999); } new_cursor_row -= y_scroll; render_buf += render_start; render_len = render_end - render_start; Linenoise::Log("truncate to rows %d - %d (render bytes %d to %d)", y_scroll, y_scroll + ws.ws_row, render_start, render_end); rows = ws.ws_row; } else { y_scroll = 0; } /* Update maxrows if needed. */ if (rows > (int)maxrows) { maxrows = rows; } vector tokens; if (Highlighting::IsEnabled()) { bool is_dot_command = buf[0] == '.'; auto match = search_index < search_matches.size() ? &search_matches[search_index] : nullptr; tokens = Highlighting::Tokenize(render_buf, render_len, is_dot_command, match); // add error highlighting AddErrorHighlighting(render_start, render_end, tokens); // add completion hint if (AddCompletionMarker(render_buf, render_len, highlight_buffer, tokens)) { render_buf = (char *)highlight_buffer.c_str(); render_len = highlight_buffer.size(); } } if (rows > 1) { // add continuation markers highlight_buffer = AddContinuationMarkers(render_buf, render_len, plen, y_scroll > 0 ? new_cursor_row + 1 : new_cursor_row, tokens); render_buf = (char *)highlight_buffer.c_str(); render_len = highlight_buffer.size(); } if (duckdb::Utf8Proc::IsValid(render_buf, render_len)) { if (Highlighting::IsEnabled()) { highlight_buffer = Highlighting::HighlightText(render_buf, render_len, 0, render_len, tokens); render_buf = (char *)highlight_buffer.c_str(); render_len = highlight_buffer.size(); } } /* First step: clear all the lines used before. To do so start by * going to the last row. */ AppendBuffer append_buffer; if (old_rows - old_cursor_rows > 0) { Linenoise::Log("go down %d", old_rows - old_cursor_rows); snprintf(seq, 64, "\x1b[%dB", old_rows - int(old_cursor_rows)); append_buffer.Append(seq); } /* Now for every row clear it, go up. */ for (int j = 0; j < old_rows - 1; j++) { Linenoise::Log("clear+up"); append_buffer.Append("\r\x1b[0K\x1b[1A"); } /* Clean the top line. */ Linenoise::Log("clear"); append_buffer.Append("\r\x1b[0K"); /* Write the prompt and the current buffer content */ if (y_scroll == 0) { append_buffer.Append(prompt); } append_buffer.Append(render_buf, render_len); /* Show hints if any. */ RefreshShowHints(append_buffer, plen); /* If we are at the very end of the screen with our prompt, we need to * emit a newline and move the prompt to the first column. */ Linenoise::Log("pos > 0 %d", pos > 0 ? 1 : 0); Linenoise::Log("pos == len %d", pos == len ? 1 : 0); Linenoise::Log("new_cursor_x == cols %d", new_cursor_x == ws.ws_col ? 1 : 0); if (pos > 0 && pos == len && new_cursor_x == ws.ws_col) { Linenoise::Log("", 0); append_buffer.Append("\n"); append_buffer.Append("\r"); rows++; new_cursor_row++; new_cursor_x = 0; if (rows > (int)maxrows) { maxrows = rows; } } Linenoise::Log("render %d rows (old rows %d)", rows, old_rows); /* Move cursor to right position. */ Linenoise::Log("new_cursor_row %d", new_cursor_row); Linenoise::Log("new_cursor_x %d", new_cursor_x); Linenoise::Log("len %d", len); Linenoise::Log("old_cursor_rows %d", old_cursor_rows); Linenoise::Log("pos %d", pos); Linenoise::Log("max cols %d", ws.ws_col); /* Go up till we reach the expected position. */ if (rows - new_cursor_row > 0) { Linenoise::Log("go-up %d", rows - new_cursor_row); snprintf(seq, 64, "\x1b[%dA", rows - new_cursor_row); append_buffer.Append(seq); } /* Set column. */ col = new_cursor_x; Linenoise::Log("set col %d", 1 + col); if (col) { snprintf(seq, 64, "\r\x1b[%dC", col); } else { snprintf(seq, 64, "\r"); } append_buffer.Append(seq); Linenoise::Log("\n"); old_cursor_rows = new_cursor_row; append_buffer.Write(fd); } /* Calls the two low level functions refreshSingleLine() or * refreshMultiLine() according to the selected mode. */ void Linenoise::RefreshLine() { if (Terminal::IsMultiline()) { RefreshMultiLine(); } else { RefreshSingleLine(); } } } // namespace duckdb