Undo/redo: - cmd_newline recorded the undo node's position after the cursor moved past the split point, so undoing Enter joined the wrong pair of lines. - Backspace/Delete line-joins recorded UndoType::Newline, whose apply() semantics are inverted for a join (forward=split); add UndoType::JoinLines with correct forward/backward behavior. - Regex replace-all, indent/unindent region, kill-to-eol, kill-line, kill-region, and delete-word-prev/next mutated the buffer via the no-undo-recording raw APIs; they now record undo, grouped atomically via a shared UndoGroupGuard. - Plain-text replace-all with an empty replacement advanced the scan position one character too far, skipping adjacent/overlapping matches. Crash recovery / memory safety: - Buffer's move ctor/assignment never carried over swap_rec_, on_disk_identity_, or the visual-line-mode fields, so the crash-recovery journal silently stopped tracking a buffer whenever Editor's std::vector<Buffer> reallocated or shifted (e.g. opening/closing files). Added SwapManager::Rehome() plus Flush()-before-mutate in Editor::AddBuffer/CloseBuffer so the journal's Buffer* key and the background writer thread never reference a stale address. - UndoTree leaked its entire node graph (including all edit text) on every buffer close/reload; added ~UndoTree() to free it. - main.cc didn't restore the terminal if an exception escaped the run loop; added an RAII guard around Frontend::Shutdown(). Input handling: - Terminal frontend used getch() instead of get_wch(), silently dropping non-ASCII keyboard input despite linking wide-char ncurses. - KKeymap's C-k lookup switches on an already-lowercased key, making `case 'E'` unreachable dead code; C-k Shift-E silently aliased to C-k e. - ImGui file picker wasn't modal: keystrokes typed while it was open still reached the buffer underneath as edit commands, and Escape didn't close it. - Gated ImGuiInputHandler's unconditional fprintf/fflush diagnostics behind an IMGUI_IH_DEBUG macro, matching QtInputHandler's existing convention. Syntax highlighting: - Go/Rust/SQL highlighters weren't stateful, so multi-line /* */ comments mis-highlighted as code starting on the second line. - Python's triple-quoted-string handler didn't re-scan the remainder of a line after a string closed mid-line, missing a same-line reopen. - HighlighterEngine's state_last_contig_ field was declared but unused, forcing an O(n) scan of state_cache_ on every stateful lookup; wired it up as a real fast path and fixed InvalidateFrom() to keep it in sync. - kge's :syntax off was silently undone the next frame because apply_syntax_to_buffer() re-applied the config default unconditionally every frame; added a user-override flag mirroring the existing edit-mode-detection guard. Added regression tests for all of the above (test_undo.cc, test_kkeymap.cc, test_command_semantics.cc, test_search_replace_flow.cc, and a new test_syntax_highlighting.cc). Full suite (163 tests) passes, including under AddressSanitizer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
187 lines
4.6 KiB
C++
187 lines
4.6 KiB
C++
#include "SqlHighlighter.h"
|
|
#include "../Buffer.h"
|
|
#include <cctype>
|
|
|
|
namespace kte {
|
|
static void
|
|
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
|
{
|
|
if (b > a)
|
|
out.push_back({a, b, k});
|
|
}
|
|
|
|
|
|
static bool
|
|
is_ident_start(char c)
|
|
{
|
|
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
|
}
|
|
|
|
|
|
static bool
|
|
is_ident_char(char c)
|
|
{
|
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
|
|
}
|
|
|
|
|
|
SqlHighlighter::SqlHighlighter()
|
|
{
|
|
const char *kw[] = {
|
|
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
|
|
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
|
|
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
|
|
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
|
|
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
|
|
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
|
|
};
|
|
for (auto s: kw)
|
|
kws_.insert(s);
|
|
|
|
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
|
|
for (auto s: types)
|
|
types_.insert(s);
|
|
}
|
|
|
|
|
|
void
|
|
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
|
{
|
|
StatefulHighlighter::LineState prev;
|
|
(void) HighlightLineStateful(buf, row, prev, out);
|
|
}
|
|
|
|
|
|
StatefulHighlighter::LineState
|
|
SqlHighlighter::HighlightLineStateful(const Buffer &buf,
|
|
int row,
|
|
const LineState &prev,
|
|
std::vector<HighlightSpan> &out) const
|
|
{
|
|
StatefulHighlighter::LineState state = prev;
|
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
|
return state;
|
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
|
int n = static_cast<int>(s.size());
|
|
int i = 0;
|
|
|
|
// Continue a multi-line block comment from the previous line.
|
|
if (state.in_block_comment) {
|
|
int j = i;
|
|
while (i + 1 < n) {
|
|
if (s[i] == '*' && s[i + 1] == '/') {
|
|
i += 2;
|
|
push(out, j, i, TokenKind::Comment);
|
|
state.in_block_comment = false;
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
if (state.in_block_comment) {
|
|
push(out, j, n, TokenKind::Comment);
|
|
return state;
|
|
}
|
|
}
|
|
|
|
while (i < n) {
|
|
char c = s[i];
|
|
if (c == ' ' || c == '\t') {
|
|
int j = i + 1;
|
|
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
|
++j;
|
|
push(out, i, j, TokenKind::Whitespace);
|
|
i = j;
|
|
continue;
|
|
}
|
|
// line comments: -- ...
|
|
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
|
|
push(out, i, n, TokenKind::Comment);
|
|
break;
|
|
}
|
|
// block comment: /* ... */ (may span multiple lines)
|
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
|
int j = i + 2;
|
|
bool closed = false;
|
|
while (j + 1 <= n) {
|
|
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
|
j += 2;
|
|
closed = true;
|
|
break;
|
|
}
|
|
++j;
|
|
}
|
|
if (!closed) {
|
|
push(out, i, n, TokenKind::Comment);
|
|
state.in_block_comment = true;
|
|
return state;
|
|
} else {
|
|
push(out, i, j, TokenKind::Comment);
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
// strings: '...' or "..."
|
|
if (c == '\'' || c == '"') {
|
|
char q = c;
|
|
int j = i + 1;
|
|
bool esc = false;
|
|
while (j < n) {
|
|
char d = s[j++];
|
|
if (d == q) {
|
|
// Handle doubled quote escaping for SQL single quotes
|
|
if (q == '\'' && j < n && s[j] == '\'') {
|
|
++j;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (d == '\\') {
|
|
esc = !esc;
|
|
} else {
|
|
esc = false;
|
|
}
|
|
}
|
|
push(out, i, j, TokenKind::String);
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (std::isdigit(static_cast<unsigned char>(c))) {
|
|
int j = i + 1;
|
|
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
|
++j;
|
|
push(out, i, j, TokenKind::Number);
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (is_ident_start(c)) {
|
|
int j = i + 1;
|
|
while (j < n && is_ident_char(s[j]))
|
|
++j;
|
|
std::string id = s.substr(i, j - i);
|
|
std::string lower;
|
|
lower.reserve(id.size());
|
|
for (char ch: id)
|
|
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
|
TokenKind k = TokenKind::Identifier;
|
|
if (kws_.count(lower))
|
|
k = TokenKind::Keyword;
|
|
else if (types_.count(lower))
|
|
k = TokenKind::Type;
|
|
push(out, i, j, k);
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (std::ispunct(static_cast<unsigned char>(c))) {
|
|
TokenKind k = TokenKind::Operator;
|
|
if (c == ',' || c == ';' || c == '(' || c == ')')
|
|
k = TokenKind::Punctuation;
|
|
push(out, i, i + 1, k);
|
|
++i;
|
|
continue;
|
|
}
|
|
push(out, i, i + 1, TokenKind::Default);
|
|
++i;
|
|
}
|
|
return state;
|
|
}
|
|
} // namespace kte
|