should be it

This commit is contained in:
2025-10-24 19:21:19 -05:00
parent a4b23fc57c
commit f09560c7b1
14047 changed files with 3161551 additions and 1 deletions

View File

@@ -0,0 +1,975 @@
#include "linenoise.hpp"
#include "highlighting.hpp"
#include "history.hpp"
#include "utf8proc_wrapper.hpp"
#include <unistd.h>
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<highlightToken> &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<highlightToken> 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<highlightToken> &tokens) {
vector<highlightToken> 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<idx_t> &brackets, vector<idx_t> &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<idx_t> &brackets, vector<idx_t> &cursor_brackets, idx_t pos, idx_t i,
vector<idx_t> &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<idx_t> &brackets, vector<idx_t> &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<highlightToken> &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<idx_t> brackets; // ()
vector<idx_t> square_brackets; // []
vector<idx_t> curly_brackets; // {}
vector<idx_t> errors;
vector<idx_t> cursor_brackets;
vector<idx_t> comment_start;
vector<idx_t> 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<highlightToken> 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<highlightToken> &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<highlightToken> 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("<newline>", 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