From 43f443e12802c01829d44b14db65348e7ccac6e0 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Mon, 22 Dec 2025 17:18:45 +0000 Subject: [PATCH] Add virtual text support in editor rendering --- include/editor.h | 29 +++++++ src/editor.cc | 206 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 218 insertions(+), 17 deletions(-) diff --git a/include/editor.h b/include/editor.h index 0cead51..d6cc2a5 100644 --- a/include/editor.h +++ b/include/editor.h @@ -98,6 +98,32 @@ struct SpanCursor { } }; +struct VHint { + Coord pos; + char *text; // Can only be a single line with ascii only + uint32_t len; + + bool operator<(const VHint &other) const { return pos < other.pos; } +}; + +struct VWarn { + uint32_t line; + char *text; // Can only be a single line + uint32_t len; + int8_t type; // For hl + + bool operator<(const VWarn &other) const { return line < other.line; } +}; + +struct VAI { + Coord pos; + char *text; + uint32_t len; + uint32_t lines; // number of \n in text for speed .. the ai part will not + // line wrap but multiline ones need to have its own lines + // after the first one +}; + struct Editor { const char *filename; Knot *root; @@ -121,6 +147,9 @@ struct Editor { Spans def_spans; uint32_t hooks[94]; bool jumper_set; + std::vector hints; + std::vector warnings; + VAI ai; }; inline const Fold *fold_for_line(const std::vector &folds, diff --git a/src/editor.cc b/src/editor.cc index 72270a6..e657c91 100644 --- a/src/editor.cc +++ b/src/editor.cc @@ -56,6 +56,25 @@ void render_editor(Editor *editor) { v.push_back({editor->hooks[i], '!' + i}); std::sort(v.begin(), v.end()); auto hook_it = v.begin(); + while (hook_it != v.end() && hook_it->first <= editor->scroll.row) + ++hook_it; + + // Iterators for hints and warnings (both already sorted) + size_t hint_idx = 0; + size_t warn_idx = 0; + + // Helper to advance hint iterator to current line + auto advance_hints_to = [&](uint32_t row) { + while (hint_idx < editor->hints.size() && + editor->hints[hint_idx].pos.row < row) + ++hint_idx; + }; + auto advance_warns_to = [&](uint32_t row) { + while (warn_idx < editor->warnings.size() && + editor->warnings[warn_idx].line < row) + ++warn_idx; + }; + std::shared_lock knot_lock(editor->knot_mtx); if (editor->selection_active) { Coord start, end; @@ -105,6 +124,47 @@ void render_editor(Editor *editor) { sel_start = line_to_byte(editor->root, start.row, nullptr) + start.col; sel_end = line_to_byte(editor->root, end.row, nullptr) + end.col; } + + // Helper for warning colors based on type + auto warn_colors = [](int8_t type) -> std::pair { + switch (type) { + case 1: // info + return {0x7fbfff, 0}; + case 2: // warn + return {0xffd166, 0}; + case 3: // error + return {0xff5f5f, 0}; + default: // neutral + return {0xaaaaaa, 0}; + } + }; + + // Helper to get nth line (0-based) from VAI text (ASCII/UTF-8) + auto ai_line_span = [&](const VAI &ai, + uint32_t n) -> std::pair { + const char *p = ai.text; + uint32_t remaining = ai.len; + uint32_t line_no = 0; + const char *start = p; + uint32_t len = 0; + for (uint32_t i = 0; i < ai.len; i++) { + if (ai.text[i] == '\n') { + if (line_no == n) { + len = i - (start - ai.text); + return {start, len}; + } + line_no++; + start = ai.text + i + 1; + } + } + // last line (no trailing newline) + if (line_no == n) { + len = ai.text + ai.len - start; + return {start, len}; + } + return {nullptr, 0}; + }; + Coord cursor = {UINT32_MAX, UINT32_MAX}; uint32_t line_index = editor->scroll.row; SpanCursor span_cursor(editor->spans); @@ -116,7 +176,15 @@ void render_editor(Editor *editor) { uint32_t global_byte_offset = line_to_byte(editor->root, line_index, nullptr); span_cursor.sync(global_byte_offset); def_span_cursor.sync(global_byte_offset); + + const bool ai_active = editor->ai.text && editor->ai.len > 0; + const uint32_t ai_row = ai_active ? editor->ai.pos.row : UINT32_MAX; + const uint32_t ai_lines = ai_active ? editor->ai.lines : 0; + while (rendered_rows < editor->size.row) { + advance_hints_to(line_index); + advance_warns_to(line_index); + const Fold *fold = fold_for_line(editor->folds, line_index); if (fold) { update(editor->position.row + rendered_rows, editor->position.col, "", @@ -140,6 +208,14 @@ void render_editor(Editor *editor) { uint32_t skip_until = fold->end; while (line_index <= skip_until) { + if (hook_it != v.end() && hook_it->first == line_index + 1) + hook_it++; + if (hint_idx < editor->hints.size() && + editor->hints[hint_idx].pos.row == line_index) + hint_idx++; + if (warn_idx < editor->warnings.size() && + editor->warnings[warn_idx].line == line_index) + warn_idx++; uint32_t line_len; char *line = next_line(it, &line_len); if (!line) @@ -161,19 +237,23 @@ void render_editor(Editor *editor) { uint32_t current_byte_offset = 0; if (rendered_rows == 0) current_byte_offset += editor->scroll.col; - while (current_byte_offset < line_len && rendered_rows < editor->size.row) { + + // AI handling: determine if this line is overridden by AI + bool ai_this_line = + ai_active && line_index >= ai_row && line_index <= ai_row + ai_lines; + bool ai_first_line = ai_this_line && line_index == ai_row; + + while ((ai_this_line ? current_byte_offset <= line_len + : current_byte_offset < line_len) && + rendered_rows < editor->size.row) { uint32_t color = editor->cursor.row == line_index ? 0x222222 : 0; if (current_byte_offset == 0 || rendered_rows == 0) { const char *hook = nullptr; char h[2] = {0, 0}; - auto it2 = hook_it; - for (; it2 != v.end(); ++it2) { - if (it2->first == line_index + 1) { - h[0] = it2->second; - hook = h; - hook_it = it2; - break; - } + if (hook_it != v.end() && hook_it->first == line_index + 1) { + h[0] = hook_it->second; + hook = h; + hook_it++; } update(editor->position.row + rendered_rows, editor->position.col, hook, 0xAAAAAA, 0, 0); @@ -194,7 +274,71 @@ void render_editor(Editor *editor) { uint32_t col = 0; uint32_t local_render_offset = 0; uint32_t line_left = line_len - current_byte_offset; + + // For AI extra lines (line > ai_row), we don't render real text + if (ai_this_line && !ai_first_line) { + const uint32_t ai_line_no = line_index - ai_row; + auto [aptr, alen] = ai_line_span(editor->ai, ai_line_no); + if (aptr && alen) { + uint32_t draw = std::min(alen, render_width); + update(editor->position.row + rendered_rows, render_x, + std::string(aptr, draw).c_str(), 0x666666, 0, CF_ITALIC); + col = draw; + } + while (col < render_width) { + update(editor->position.row + rendered_rows, render_x + col, " ", 0, + 0 | color, 0); + col++; + } + rendered_rows++; + break; // move to next screen row + } + while (line_left > 0 && col < render_width) { + // Render pending hints at this byte offset + while (hint_idx < editor->hints.size() && + editor->hints[hint_idx].pos.row == line_index && + editor->hints[hint_idx].pos.col == + current_byte_offset + local_render_offset) { + const VHint &vh = editor->hints[hint_idx]; + uint32_t draw = std::min(vh.len, render_width - col); + if (draw == 0) + break; + update(editor->position.row + rendered_rows, render_x + col, + std::string(vh.text, draw).c_str(), 0x777777, 0 | color, + CF_ITALIC); + col += draw; + ++hint_idx; + if (col >= render_width) + break; + } + if (col >= render_width) + break; + + // AI first line: stop underlying text at ai.pos.col, then render AI, + // clip + if (ai_first_line && + (current_byte_offset + local_render_offset) >= editor->ai.pos.col) { + // render AI first line + auto [aptr, alen] = ai_line_span(editor->ai, 0); + if (aptr && alen) { + uint32_t draw = std::min(alen, render_width - col); + update(editor->position.row + rendered_rows, render_x + col, + std::string(aptr, draw).c_str(), 0x666666, 0 | color, + CF_ITALIC); + col += draw; + } + // fill rest and break + while (col < render_width) { + update(editor->position.row + rendered_rows, render_x + col, " ", 0, + 0 | color, 0); + col++; + } + rendered_rows++; + current_byte_offset = line_len; // hide rest of real text + goto after_line_body; + } + if (line_index == editor->cursor.row && editor->cursor.col == (current_byte_offset + local_render_offset)) { cursor.row = editor->position.row + rendered_rows; @@ -233,11 +377,14 @@ void render_editor(Editor *editor) { update(editor->position.row + rendered_rows, render_x + col - width, "\x1b", fg, bg | color, fl); } + if (line_index == editor->cursor.row && editor->cursor.col == (current_byte_offset + local_render_offset)) { cursor.row = editor->position.row + rendered_rows; cursor.col = render_x + col; } + + // Trailing selection block if (editor->selection_active && global_byte_offset + line_len + 1 > sel_start && global_byte_offset + line_len + 1 <= sel_end && col < render_width) { @@ -245,6 +392,20 @@ void render_editor(Editor *editor) { 0x555555 | color, 0); col++; } + + // Render warning text at end (does not affect wrapping) + if (warn_idx < editor->warnings.size() && + editor->warnings[warn_idx].line == line_index && col < render_width) { + const VWarn &w = editor->warnings[warn_idx]; + auto [wfg, wbg] = warn_colors(w.type); + uint32_t draw = std::min(w.len, render_width - col); + if (draw) + update(editor->position.row + rendered_rows, render_x + col, + std::string(w.text, draw).c_str(), wfg, wbg | color, + CF_ITALIC); + // do not advance col for padding skip; we still fill remaining spaces + } + while (col < render_width) { update(editor->position.row + rendered_rows, render_x + col, " ", 0, 0 | color, 0); @@ -252,20 +413,20 @@ void render_editor(Editor *editor) { } rendered_rows++; current_byte_offset += local_render_offset; + + after_line_body: + break; // proceed to next screen row } + if (line_len == 0 || (current_byte_offset >= line_len && rendered_rows == 0)) { uint32_t color = editor->cursor.row == line_index ? 0x222222 : 0; const char *hook = nullptr; char h[2] = {0, 0}; - auto it2 = hook_it; - for (; it2 != v.end(); ++it2) { - if (it2->first == line_index + 1) { - h[0] = it2->second; - hook = h; - hook_it = it2; - break; - } + if (hook_it != v.end() && hook_it->first == line_index + 1) { + h[0] = hook_it->second; + hook = h; + hook_it++; } update(editor->position.row + rendered_rows, editor->position.col, hook, 0xAAAAAA, 0, 0); @@ -289,6 +450,17 @@ void render_editor(Editor *editor) { 0x555555 | color, 0); col++; } + // warning on empty line + if (warn_idx < editor->warnings.size() && + editor->warnings[warn_idx].line == line_index && col < render_width) { + const VWarn &w = editor->warnings[warn_idx]; + auto [wfg, wbg] = warn_colors(w.type); + uint32_t draw = std::min(w.len, render_width - col); + if (draw) + update(editor->position.row + rendered_rows, render_x + col, + std::string(w.text, draw).c_str(), wfg, wbg | color, + CF_ITALIC); + } while (col < render_width) { update(editor->position.row + rendered_rows, render_x + col, " ", 0, 0 | color, 0);