421 lines
13 KiB
C
421 lines
13 KiB
C
/**
|
|
* @file output.c
|
|
* @brief Screen rendering and output module for the Beluga text editor
|
|
* @details Handles all screen updates, cursor positioning, status bar
|
|
* rendering, and display synchronization using ANSI escape sequences
|
|
*/
|
|
|
|
#include "../include/output.h"
|
|
#include "../include/append_buffer.h"
|
|
#include "../include/buffer.h"
|
|
#include "../include/editor_op.h"
|
|
#include "../include/data.h"
|
|
#include "../include/define.h"
|
|
#include "../include/row_op.h"
|
|
#include "../include/split_screen.h"
|
|
#include "../include/syntax_highlighter.h"
|
|
#include "../include/terminal.h"
|
|
#include "../include/lsp_ui.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "include/completion.h"
|
|
#include "include/utils.h"
|
|
|
|
/**
|
|
* @brief Renders a single pane with its buffer content
|
|
*/
|
|
static void editorDrawPane(struct abuf* ab, EditorPane* pane)
|
|
{
|
|
int file_row;
|
|
char pos_buf[32];
|
|
int pos_len;
|
|
int byte_len_to_print;
|
|
int bytes_to_print;
|
|
char* highlighted;
|
|
|
|
if (pane == NULL || pane->buffer_id < 0)
|
|
return;
|
|
|
|
const struct buffer_t* buf = bufferFindById(pane->buffer_id);
|
|
if (buf == NULL)
|
|
return;
|
|
|
|
// Draw all pane
|
|
for (int y = 0; y < pane->height; y++)
|
|
{
|
|
file_row = y + pane->y_offset;
|
|
|
|
// Set cursor at start of pane row
|
|
pos_len = snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH",
|
|
pane->origin_y + y + 1, pane->origin_x + 1);
|
|
abAppend(ab, pos_buf, pos_len);
|
|
|
|
// Apply background color (6 bytes for RGB format)
|
|
abAppend(ab, E.theme.BACKGROUND_COLOR, (int)strlen(E.theme.BACKGROUND_COLOR));
|
|
|
|
|
|
// pane line is out of buffer
|
|
if (file_row >= buf->numrows)
|
|
{
|
|
// Empty line - show tilde
|
|
|
|
abAppend(ab, "~", 1);
|
|
for (int i = 0;
|
|
i < pane->width - 1;
|
|
++i)
|
|
{
|
|
abAppend(ab, " ", 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (E.constantes.LSP) {
|
|
lspUiDrawGutter(ab, &E.lsp_diagnostics, pane->buffer_id, file_row);
|
|
}
|
|
|
|
|
|
if (buf->filename[strlen(buf->filename) - 1] == 'c' || buf->filename[strlen(buf->filename) - 1] == 'h')
|
|
{
|
|
// Render line with syntax highlighting, constrain to pane width
|
|
highlighted = highlight_line(
|
|
&buf->row[file_row].chars[pane->x_offset], &byte_len_to_print);
|
|
|
|
// Print only up to pane width
|
|
abAppend(ab, highlighted, byte_len_to_print);
|
|
bFree(highlighted);
|
|
}
|
|
else
|
|
{
|
|
// Render basic line
|
|
bytes_to_print =
|
|
buf->row[file_row].size - pane->x_offset < pane->width
|
|
? buf->row[file_row].size - pane->x_offset
|
|
: pane->width;
|
|
abAppend(ab, &buf->row[file_row].chars[pane->x_offset], bytes_to_print);
|
|
}
|
|
// Fill remaining space with background color to pane width
|
|
for (int i = 0;
|
|
i < pane->width - editorRowCharCount(&buf->row[file_row], pane->width) + pane->x_offset;
|
|
++i)
|
|
{
|
|
abAppend(ab, " ", 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Renders all panes based on current split configuration
|
|
*/
|
|
static void editorDrawAllPanes(struct abuf* ab)
|
|
{
|
|
const ScreenLayout* layout = splitScreenGetLayout();
|
|
|
|
if (layout->num_panes == 1)
|
|
{
|
|
// Single pane fullscreen
|
|
editorDrawPane(ab, &layout->panes[0]);
|
|
}
|
|
else if (layout->num_panes == 2)
|
|
{
|
|
// Draw both panes
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
editorDrawPane(ab, &layout->panes[i]);
|
|
|
|
// Draw pane border/divider if not the last pane
|
|
if (layout->mode == SPLIT_VERTICAL && i == 0)
|
|
{
|
|
// Draw vertical divider
|
|
int divider_col = layout->panes[0].width;
|
|
for (int y = 0; y < layout->panes[0].height; y++)
|
|
{
|
|
char pos_buf[32];
|
|
snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH", y + 1, divider_col);
|
|
abAppend(ab, pos_buf, (int)strlen(pos_buf));
|
|
abAppend(ab, "\x1b[1m|\x1b[0m", 9); // Bold pipe divider
|
|
}
|
|
}
|
|
else if (layout->mode == SPLIT_HORIZONTAL && i == 0)
|
|
{
|
|
// Draw horizontal divider
|
|
int divider_row = layout->panes[0].height;
|
|
char pos_buf[32];
|
|
snprintf(pos_buf, sizeof(pos_buf), "\x1b[%d;%dH", divider_row + 1, 1);
|
|
abAppend(ab, pos_buf, (int)strlen(pos_buf));
|
|
for (int x = 0; x < E.screencols; x++)
|
|
{
|
|
abAppend(ab, "\x1b[1m-\x1b[0m", 9); // Bold dash divider
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Updates scroll offsets to keep cursor visible on screen
|
|
* @details Adjusts E.row_offset and E.col_offset to ensure the cursor remains
|
|
* within the visible viewport. Also updates E.rx (rendered x-coordinate).
|
|
* @note Updates global editor state E
|
|
* @see editorRowCxToRx()
|
|
*/
|
|
void editorScroll()
|
|
{
|
|
EditorPane* active = splitScreenGetActivePane();
|
|
struct buffer_t* buf = bufferFindById(active->buffer_id);
|
|
int rel_x, rel_y;
|
|
|
|
// compute relative coordinates
|
|
rel_x = editorRowCharCount(&buf->row[buf->y], buf->x);
|
|
appDebug("counting %d\n", rel_x);
|
|
rel_y = buf->y;
|
|
|
|
appDebug("%d %d / %d %d\n", active->cursor_x, active->x_offset,
|
|
active->cursor_y, active->y_offset);
|
|
|
|
while (rel_x != active->cursor_x + active->x_offset ||
|
|
rel_y != active->cursor_y + active->y_offset)
|
|
{
|
|
if (rel_x < active->cursor_x + active->x_offset)
|
|
{
|
|
// LEFT
|
|
if (active->cursor_x == 0 && active->x_offset)
|
|
{
|
|
active->x_offset--;
|
|
}
|
|
else
|
|
{
|
|
active->cursor_x--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// RIGHT
|
|
if (rel_x > active->cursor_x + active->x_offset)
|
|
{
|
|
if (active->cursor_x == active->width - 1)
|
|
{
|
|
active->x_offset++;
|
|
}
|
|
else
|
|
{
|
|
active->cursor_x++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rel_y < active->cursor_y + active->y_offset)
|
|
{
|
|
if (active->cursor_y == 0 && active->y_offset)
|
|
{
|
|
active->y_offset--;
|
|
}
|
|
else
|
|
{
|
|
active->cursor_y--;
|
|
}
|
|
}
|
|
|
|
if (rel_y > active->cursor_y + active->y_offset)
|
|
{
|
|
if (active->cursor_y == active->height - 1)
|
|
{
|
|
active->y_offset++;
|
|
}
|
|
else
|
|
{
|
|
active->cursor_y++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
char* basename(char* path)
|
|
{
|
|
int len = (int)strlen(path);
|
|
|
|
|
|
for (int i = len - 1; i > 0; i--)
|
|
{
|
|
if (path[i] == '/')
|
|
{
|
|
path = path + i + 1;
|
|
break;
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* @brief Renders the status bar at the bottom of the screen
|
|
* @details Displays filename, line count, dirty flag, and current cursor
|
|
* position in an inverted color bar. Right-aligns the cursor position
|
|
* indicator.
|
|
* @param ab Pointer to append buffer structure for accumulating output
|
|
* @note Uses ANSI escape codes for color inversion
|
|
*/
|
|
void editorDrawStatusBar(struct abuf* ab)
|
|
{
|
|
// TO MODIFY
|
|
int len, render_len;
|
|
char status[E.screencols], render_status[E.screencols * 4];
|
|
EditorPane* active = splitScreenGetActivePane();
|
|
struct buffer_t* buf = bufferFindById(active->buffer_id);
|
|
|
|
abAppend(ab, "\x1b[7m", 4); // inverting colors
|
|
|
|
const char* mode_str = "";
|
|
ScreenLayout* layout = splitScreenGetLayout();
|
|
if (layout->mode == SPLIT_VERTICAL)
|
|
mode_str = " [V-SPLIT]";
|
|
else if (layout->mode == SPLIT_HORIZONTAL)
|
|
mode_str = " [H-SPLIT]";
|
|
|
|
// Build buffer status showing all buffers with dirty indicators
|
|
char buf_status[1024] = "";
|
|
int offset = 0;
|
|
for (int i = 0; i < E.number_of_buffer; i++)
|
|
{
|
|
struct buffer_t* b = &E.buffers[i];
|
|
char marker = (b->buffer_id == active->buffer_id) ? '>' : ' ';
|
|
char dirty_marker = b->dirty ? '*' : ' ';
|
|
offset += snprintf(&buf_status[offset], sizeof(buf_status) - offset,
|
|
"%c%d:%s%c ", marker, b->buffer_id,
|
|
b->filename ? basename(b->filename) : "[No Name]", dirty_marker);
|
|
}
|
|
|
|
len = snprintf(status, sizeof(status), "%s%s", buf_status, mode_str);
|
|
|
|
render_len = snprintf(render_status, sizeof(render_status), "%d/%d",
|
|
active->cursor_y + 1, buf->numrows);
|
|
|
|
if (len > E.screencols)
|
|
{
|
|
len = E.screencols;
|
|
}
|
|
|
|
abAppend(ab, status, len);
|
|
while (len < E.screencols)
|
|
{
|
|
if (E.screencols - len == render_len + 1)
|
|
{
|
|
abAppend(ab, render_status, render_len);
|
|
break;
|
|
}
|
|
abAppend(ab, " ", 1);
|
|
++len;
|
|
}
|
|
|
|
abAppend(ab, "\x1b[m", 3); // normal text mode
|
|
abAppend(ab, "\r\n", 2);
|
|
}
|
|
|
|
/**
|
|
* @brief Renders the message bar below the status bar
|
|
* @details Displays temporary status messages for a limited time (5 seconds).
|
|
* Only displays message if within time window and within screen width.
|
|
* @param ab Pointer to append buffer structure for accumulating output
|
|
* @note Messages are set by editorSetStatusMessage()
|
|
*/
|
|
void editorDrawMessageBar(struct abuf* ab)
|
|
{
|
|
int msg_len = (int)strlen(E.status_msg);
|
|
abAppend(ab, ERASE_END_LINE, 3);
|
|
if (msg_len > E.screencols)
|
|
{
|
|
msg_len = E.screencols;
|
|
}
|
|
if (msg_len && time(NULL) - E.status_msg_time < 5)
|
|
{
|
|
abAppend(ab, E.status_msg, msg_len);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Performs complete screen refresh and buffer synchronization
|
|
* @details Clears screen, redraws all visible content (rows, status bar,
|
|
* message bar), positions cursor, and writes accumulated buffer to stdout. This
|
|
* is the main rendering function called each frame.
|
|
* @note Updates global editor state E (via editorScroll())
|
|
* @see editorDrawRows()
|
|
* @see editorDrawStatusBar()
|
|
* @see editorDrawMessageBar()
|
|
*/
|
|
|
|
void editorRefreshScreen()
|
|
{
|
|
struct abuf ab = ABUF_INIT;
|
|
char buf[32];
|
|
|
|
abAppend(&ab, HIDE_CURSOR, 6);
|
|
abAppend(&ab, CURSOR_TOP_LEFT, 3);
|
|
abAppend(&ab, E.theme.BACKGROUND_COLOR,
|
|
(int)strlen(E.theme.BACKGROUND_COLOR));
|
|
|
|
editorScroll();
|
|
|
|
|
|
EditorPane* active = splitScreenGetActivePane();
|
|
struct buffer_t* buffer = bufferFindById(active->buffer_id);
|
|
|
|
editorDrawAllPanes(&ab);
|
|
if (E.constantes.LSP) {
|
|
|
|
// ── LSP: draw completion popup every frame while visible ──────────────────
|
|
appDebug("[REFRESH] lsp_completion.visible=%d\n",
|
|
E.lsp_completion.visible);
|
|
while (E.lsp_client->completion_requested && !E.lsp_client->completion_just_arrived);
|
|
// reset flags
|
|
E.lsp_client->completion_just_arrived = 0;
|
|
E.lsp_client->completion_requested = 0;
|
|
|
|
// ── LSP: diagnostic for current line in status bar ────────────────────────
|
|
const char* diag = lspUiDiagnosticAtCursor(
|
|
&E.lsp_diagnostics,
|
|
active->buffer_id,
|
|
buffer->y);
|
|
if (diag) {
|
|
char single_line[512];
|
|
int i = 0;
|
|
|
|
// Copy until newline, \0, or screen width
|
|
while (diag[i] && diag[i] != '\n' && i < E.screencols - 4)
|
|
{
|
|
single_line[i] = diag[i];
|
|
i++;
|
|
}
|
|
|
|
// If message was truncated, add ellipsis
|
|
if (diag[i] != '\0' && diag[i] != '\n')
|
|
{
|
|
single_line[i++] = '.';
|
|
single_line[i++] = '.';
|
|
single_line[i++] = '.';
|
|
}
|
|
single_line[i] = '\0';
|
|
editorSetStatusMessage("● %s", single_line);
|
|
}
|
|
|
|
}
|
|
editorDrawStatusBar(&ab);
|
|
editorDrawMessageBar(&ab);
|
|
if (E.constantes.LSP && (E.lsp_client && E.lsp_client->state == LSP_READY))
|
|
{
|
|
lspUiDrawCompletion(&ab, &E.lsp_completion);
|
|
}
|
|
|
|
|
|
// ── Position cursor (account for gutter width) ────────────────────────────
|
|
snprintf(buf, sizeof(buf), "\x1b[%d;%dH",
|
|
active->cursor_y + active->origin_y + 1,
|
|
active->cursor_x + active->origin_x + 1 + (E.constantes.LSP ? GUTTER_WIDTH : 0));
|
|
abAppend(&ab, buf, (int)strlen(buf));
|
|
|
|
abAppend(&ab, SHOW_CURSOR, 6);
|
|
write(STDOUT_FILENO, ab.b, ab.len);
|
|
abFree(&ab);
|
|
}
|