Switch to c++

This commit is contained in:
2025-12-07 17:22:12 +00:00
parent 2927df7710
commit 7acf35c8ea
55 changed files with 3506 additions and 304 deletions

View File

508
src/editor.cc Normal file
View File

@@ -0,0 +1,508 @@
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
}
#include "../include/editor.h"
#include "../include/ts.h"
#include "../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h"
#include <cstdint>
#include <fstream>
char *load_file(const char *path, uint32_t *out_len) {
std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate);
if (!file.is_open())
return nullptr;
std::streamsize len = file.tellg();
if (len < 0 || (std::uint32_t)len > 0xFFFFFFFF)
return nullptr;
file.seekg(0, std::ios::beg);
char *buf = (char *)malloc(static_cast<std::uint32_t>(len));
if (!buf)
return nullptr;
if (file.read(buf, len)) {
*out_len = static_cast<uint32_t>(len);
return buf;
} else {
free(buf);
return nullptr;
}
}
Editor *new_editor(const char *filename, Coord position, Coord size) {
Editor *editor = new Editor();
if (!editor)
return nullptr;
uint32_t len = 0;
char *str = load_file(filename, &len);
if (!str)
return nullptr;
editor->filename = filename;
editor->position = position;
editor->size = size;
editor->tree = nullptr;
editor->cursor = {0, 0};
editor->selection_active = false;
editor->selection = {0, 0};
editor->scroll = {0, 0};
editor->root = load(str, len, optimal_chunk_size(len));
editor->folded = (int *)calloc(editor->root->line_count + 2, sizeof(int));
std::string query = get_exe_dir() + "/../grammar/ruby.scm";
if (!(len > (1024 * 1024))) {
editor->parser = ts_parser_new();
editor->language = tree_sitter_ruby();
ts_parser_set_language(editor->parser, editor->language);
editor->query = load_query(query.c_str(), editor);
}
free(str);
return editor;
}
void free_editor(Editor *editor) {
if (editor->folded)
free(editor->folded);
ts_parser_delete(editor->parser);
if (editor->tree)
ts_tree_delete(editor->tree);
if (editor->query)
ts_query_delete(editor->query);
free_rope(editor->root);
delete editor;
}
void scroll_up(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
uint32_t count = 0;
uint32_t visible_lines_checked = 0;
int32_t current_check_row = editor->scroll.row;
while (visible_lines_checked < number + 1 && current_check_row >= 0) {
if (editor->folded[current_check_row] != 1)
visible_lines_checked++;
count++;
current_check_row--;
}
if (current_check_row < 0)
count = editor->scroll.row;
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row - count + 1);
std::vector<std::pair<uint32_t, uint32_t>> stack;
stack.reserve(count);
uint32_t lines_iterated = 0;
uint32_t start_row = editor->scroll.row - count + 1;
while (lines_iterated < count) {
char *line_content = next_line(it);
uint32_t current_idx = start_row + lines_iterated;
int fold_state = editor->folded[current_idx];
if (fold_state == 2) {
stack.push_back({1, current_idx});
} else if (fold_state == 0) {
uint32_t len =
(line_content != nullptr) ? grapheme_strlen(line_content) : 0;
stack.push_back({len, current_idx});
}
if (line_content)
free(line_content);
lines_iterated++;
}
uint32_t ln = 0;
uint32_t wrap_limit = editor->size.col;
for (int i = stack.size() - 1; i >= 0; i--) {
uint32_t len = stack[i].first;
uint32_t row_idx = stack[i].second;
uint32_t segments =
(wrap_limit > 0 && len > 0) ? (len + wrap_limit - 1) / wrap_limit : 1;
if (len == 0)
segments = 1;
for (int seg = (row_idx == editor->scroll.row)
? editor->scroll.col / wrap_limit
: segments - 1;
seg >= 0; seg--) {
ln++;
if (ln == number + 1) {
editor->scroll.row = row_idx;
editor->scroll.col = seg * wrap_limit;
free(it);
return;
}
}
}
if (ln < number + 1) {
editor->scroll.row = 0;
editor->scroll.col = 0;
}
free(it);
}
void scroll_down(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row);
uint32_t current_row = editor->scroll.row;
uint32_t lines_moved = 0;
uint32_t wrap_limit = editor->size.col;
if (wrap_limit == 0)
wrap_limit = 1;
while (lines_moved < number) {
char *line_content = next_line(it);
if (line_content == nullptr)
break;
int fold_state = editor->folded[current_row];
if (fold_state == 1) {
free(line_content);
current_row++;
continue;
}
uint32_t segments = 1;
if (fold_state == 2) {
segments = 1;
} else {
uint32_t len = grapheme_strlen(line_content);
segments = (len > 0) ? (len + wrap_limit - 1) / wrap_limit : 1;
}
uint32_t start_seg = (current_row == editor->scroll.row)
? (editor->scroll.col / wrap_limit)
: 0;
for (uint32_t seg = start_seg; seg < segments; seg++) {
if (current_row == editor->scroll.row && seg == start_seg)
continue;
lines_moved++;
if (lines_moved == number) {
editor->scroll.row = current_row;
editor->scroll.col = seg * wrap_limit;
free(line_content);
free(it);
return;
}
}
free(line_content);
current_row++;
}
free(it);
}
void cursor_down(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line_content = next_line(it);
if (line_content == nullptr)
return;
uint32_t visual_col =
get_visual_col_from_bytes(line_content, editor->cursor.col);
do {
free(line_content);
line_content = next_line(it);
editor->cursor.row += 1;
if (editor->cursor.row >= editor->root->line_count) {
editor->cursor.row = editor->root->line_count - 1;
break;
};
if (editor->folded && editor->folded[editor->cursor.row] != 0)
number++;
} while (--number > 0);
free(it);
if (line_content == nullptr)
return;
editor->cursor.col = get_bytes_from_visual_col(line_content, visual_col);
free(line_content);
}
void cursor_up(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line_content = next_line(it);
if (!line_content) {
free(it);
return;
}
uint32_t visual_col =
get_visual_col_from_bytes(line_content, editor->cursor.col);
free(line_content);
while (number > 0 && editor->cursor.row > 0) {
editor->cursor.row--;
if (editor->folded && editor->folded[editor->cursor.row] != 0)
continue;
number--;
}
free(it);
it = begin_l_iter(editor->root, editor->cursor.row);
line_content = next_line(it);
if (!line_content) {
free(it);
return;
}
editor->cursor.col = get_bytes_from_visual_col(line_content, visual_col);
free(line_content);
free(it);
}
void cursor_right(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line = next_line(it);
free(it);
if (!line)
return;
uint32_t line_len = strlen(line);
if (line[line_len - 1] == '\n')
line[--line_len] = '\0';
while (number > 0) {
if (editor->cursor.col >= line_len) {
free(line);
line = nullptr;
uint32_t next_row = editor->cursor.row + 1;
while (editor->folded && next_row < editor->root->line_count &&
editor->folded[next_row] != 0)
next_row++;
if (next_row >= editor->root->line_count) {
editor->cursor.col = line_len;
break;
}
editor->cursor.row = next_row;
editor->cursor.col = 0;
it = begin_l_iter(editor->root, editor->cursor.row);
line = next_line(it);
free(it);
if (!line)
break;
line_len = strlen(line);
if (line[line_len - 1] == '\n')
line[--line_len] = '\0';
} else {
uint32_t inc = grapheme_next_character_break_utf8(
line + editor->cursor.col, line_len - editor->cursor.col);
if (inc == 0)
break;
editor->cursor.col += inc;
}
number--;
}
if (line)
free(line);
}
void cursor_left(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line = next_line(it);
free(it);
if (!line)
return;
uint32_t len = strlen(line);
if (line[len - 1] == '\n')
line[--len] = '\0';
while (number > 0) {
if (editor->cursor.col == 0) {
free(line);
line = nullptr;
if (editor->cursor.row == 0)
break;
int32_t prev_row = editor->cursor.row - 1;
while (editor->folded && prev_row >= 0 && editor->folded[prev_row] != 0)
prev_row--;
if (prev_row < 0)
break;
editor->cursor.row = prev_row;
it = begin_l_iter(editor->root, editor->cursor.row);
line = next_line(it);
free(it);
if (!line)
break;
uint32_t len = strlen(line);
if (line[len - 1] == '\n')
line[--len] = '\0';
editor->cursor.col = len;
} else {
uint32_t col = editor->cursor.col;
uint32_t new_col = 0;
uint32_t visual_col = 0;
uint32_t len = strlen(line);
while (new_col < col) {
uint32_t inc =
grapheme_next_character_break_utf8(line + new_col, len - new_col);
if (new_col + inc >= col)
break;
new_col += inc;
visual_col++;
}
editor->cursor.col = new_col;
}
number--;
}
if (line)
free(line);
}
void ensure_scroll(Editor *editor) {
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row);
uint32_t rendered_lines = 0;
uint32_t line_index = editor->scroll.row;
char *line_content = nullptr;
while (rendered_lines <= editor->size.row) {
line_content = next_line(it);
if (!line_content)
break;
char *line_content_t = line_content;
if (rendered_lines == 0)
line_content_t = line_content + editor->scroll.col;
uint32_t len = grapheme_strlen(line_content_t);
free(line_content);
uint32_t wrapped_lines = (len + editor->size.col - 1) / editor->size.col;
rendered_lines += wrapped_lines;
line_index++;
}
line_index -= 2;
free(it);
if (editor->cursor.row >= line_index && line_content)
scroll_down(editor, editor->cursor.row - line_index);
if (editor->cursor.row < editor->scroll.row)
scroll_up(editor, editor->scroll.row - editor->cursor.row);
}
void fold(Editor *editor, uint32_t start_line, uint32_t end_line) {
if (!editor)
return;
if (!editor->folded) {
editor->folded = (int *)calloc(editor->root->line_count + 2, sizeof(int));
if (!editor->folded)
return;
}
for (uint32_t i = start_line; i <= end_line && i < editor->size.row; i++)
editor->folded[i] = 1;
editor->folded[start_line] = 2;
}
void update_render_fold_marker(uint32_t row, uint32_t cols) {
const char *marker = "... folded ...";
uint32_t len = strlen(marker);
uint32_t i = 0;
for (; i < len && i < cols; i++)
update(row, i, (char[2]){marker[i], 0}, 0xc6c6c6, 0, 0);
for (; i < cols; i++)
update(row, i, " ", 0xc6c6c6, 0, 0);
}
void render_editor(Editor *editor) {
uint32_t screen_rows = editor->size.row;
uint32_t screen_cols = editor->size.col;
uint32_t line_index = editor->scroll.row;
SpanCursor span_cursor(editor->spans);
std::shared_lock knot_lock(editor->knot_mtx);
std::shared_lock span_lock(editor->span_mtx);
LineIterator *it = begin_l_iter(editor->root, line_index);
if (!it)
return;
uint32_t rendered_rows = 0;
uint32_t global_byte_offset = line_to_byte(editor->root, line_index, nullptr);
span_cursor.sync(global_byte_offset);
while (rendered_rows < screen_rows) {
if (editor->folded && editor->folded[line_index]) {
if (editor->folded[line_index] == 2) {
update_render_fold_marker(rendered_rows, screen_cols);
rendered_rows++;
}
do {
char *line = next_line(it);
if (!line)
break;
global_byte_offset += strlen(line);
if (line[strlen(line) - 1] == '\n')
global_byte_offset--;
global_byte_offset++;
free(line);
line_index++;
} while (line_index < editor->size.row &&
editor->folded[line_index] == 1);
continue;
}
char *line = next_line(it);
if (!line)
break;
uint32_t line_len = strlen(line);
if (line_len > 0 && line[line_len - 1] == '\n')
line_len--;
uint32_t current_byte_offset = 0;
if (rendered_rows == 0 && editor->scroll.col > 0) {
uint32_t skipped_cols = 0;
while (skipped_cols < editor->scroll.col &&
current_byte_offset < line_len) {
uint32_t len = grapheme_next_character_break_utf8(
line + current_byte_offset, line_len - current_byte_offset);
current_byte_offset += len;
skipped_cols++;
}
}
while (current_byte_offset < line_len && rendered_rows < screen_rows) {
uint32_t slice_byte_len = 0;
uint32_t slice_visual_cols = 0;
uint32_t probe_offset = current_byte_offset;
while (slice_visual_cols < screen_cols && probe_offset < line_len) {
uint32_t len = grapheme_next_character_break_utf8(
line + probe_offset, line_len - probe_offset);
slice_byte_len += len;
probe_offset += len;
slice_visual_cols++;
}
uint32_t col = 0;
uint32_t local_render_offset = 0;
while (local_render_offset < slice_byte_len) {
if (line_index == editor->cursor.row &&
editor->cursor.col == (current_byte_offset + local_render_offset))
set_cursor(editor->position.row + rendered_rows,
editor->position.col + col, 1);
uint32_t absolute_byte_pos =
global_byte_offset + current_byte_offset + local_render_offset;
Highlight *hl = span_cursor.get_highlight(absolute_byte_pos);
uint32_t fg = hl ? hl->fg : 0xFFFFFF;
uint32_t bg = hl ? hl->bg : 0;
uint8_t fl = hl ? hl->flags : 0;
uint32_t cluster_len = grapheme_next_character_break_utf8(
line + current_byte_offset + local_render_offset,
slice_byte_len - local_render_offset);
if (cluster_len == 0)
cluster_len = 1;
std::string cluster(line + current_byte_offset + local_render_offset,
cluster_len);
update(editor->position.row + rendered_rows, editor->position.col + col,
cluster.c_str(), fg, bg, fl);
local_render_offset += cluster_len;
col++;
}
if (line_index == editor->cursor.row &&
editor->cursor.col == (current_byte_offset + slice_byte_len))
set_cursor(editor->position.row + rendered_rows,
editor->position.col + col, 1);
while (col < screen_cols) {
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
col++;
}
rendered_rows++;
current_byte_offset += slice_byte_len;
}
if (line_len == 0 ||
(current_byte_offset >= line_len && rendered_rows == 0)) {
if (editor->cursor.row == line_index)
set_cursor(editor->position.row + rendered_rows, editor->position.col,
1);
uint32_t col = 0;
while (col < screen_cols) {
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
col++;
}
rendered_rows++;
}
global_byte_offset += line_len + 1;
line_index++;
free(line);
}
while (rendered_rows < screen_rows) {
for (uint32_t col = 0; col < screen_cols; col++)
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
rendered_rows++;
}
free(it);
}

View File

@@ -1,102 +0,0 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <string>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <vector>
#define KEY_CHAR 0
#define KEY_SPECIAL 1
#define KEY_MOUSE 2
#define KEY_NONE 3
#define KEY_UP 0
#define KEY_DOWN 1
#define KEY_LEFT 2
#define KEY_RIGHT 3
#define KEY_DELETE 4
#define KEY_ESC '\x1b'
#define PRESS 0
#define RELEASE 1
#define DRAG 2
#define SCROLL 3
#define LEFT_BTN 0
#define MIDDLE_BTN 1
#define RIGHT_BTN 2
#define SCROLL_BTN 3
#define NONE_BTN 4
#define SCROLL_UP 0
#define SCROLL_DOWN 1
#define SCROLL_LEFT 2
#define SCROLL_RIGHT 3
#define ALT 1
#define CNTRL 2
#define CNTRL_ALT 3
#define SHIFT 4
enum CellFlags : uint8_t {
CF_NONE = 0,
CF_ITALIC = 1 << 0,
CF_BOLD = 1 << 1,
CF_UNDERLINE = 1 << 2,
};
struct ScreenCell {
std::string utf8; // empty => no content
uint32_t fg = 0;
uint32_t bg = 0;
uint8_t flags = CF_NONE;
};
struct KeyEvent {
uint8_t key_type;
char c;
uint8_t special_key;
uint8_t special_modifier;
uint8_t mouse_x;
uint8_t mouse_y;
uint8_t mouse_button;
uint8_t mouse_state;
uint8_t mouse_direction;
uint8_t mouse_modifier;
};
extern int rows, cols;
extern std::vector<ScreenCell> screen; // size rows*cols
extern std::vector<ScreenCell> old_screen;
extern std::mutex screen_mutex;
struct coords {
int row;
int col;
};
extern "C" {
void get_terminal_size();
void die(const char *s);
void enable_raw_mode();
void disable_raw_mode();
void start_screen();
void end_screen();
void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg,
uint8_t flags);
void set_cursor(int row, int col, bool show_cursor_param);
void render();
coords get_size();
int real_width(std::string str);
int read_input(char *buf, size_t buflen);
KeyEvent read_key_nonblock();
KeyEvent read_key();
}

View File

@@ -1,4 +1,4 @@
#include "../headers/header.hpp"
#include "../include/ui.h"
int read_input(char *buf, size_t buflen) {
size_t i = 0;
@@ -115,11 +115,9 @@ KeyEvent read_key_nonblock() {
}
KeyEvent read_key() {
KeyEvent ret;
while (1) {
ret = read_key_nonblock();
while (true) {
KeyEvent ret = read_key_nonblock();
if (ret.key_type != KEY_NONE)
return ret;
usleep(2500);
}
}

199
src/main.cc Normal file
View File

@@ -0,0 +1,199 @@
#include "../include/editor.h"
#include "../include/ts.h"
#include "../include/ui.h"
#include "../libs/tree-sitter/lib/include/tree_sitter/api.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <sys/ioctl.h>
#include <thread>
std::atomic<bool> running{true};
Queue<KeyEvent> event_queue;
std::atomic<uint64_t> render_frames{0};
std::atomic<uint64_t> worker_frames{0};
auto start_time = std::chrono::high_resolution_clock::now();
void background_worker(Editor *editor) {
while (running) {
worker_frames++;
ts_collect_spans(editor);
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
}
void input_listener() {
while (running) {
KeyEvent event = read_key();
if (event.key_type == KEY_CHAR && event.c == CTRL('q'))
running = false;
event_queue.push(event);
}
}
void handle_editor_event(Editor *editor, KeyEvent event) {
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_DOWN)
cursor_down(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_UP)
cursor_up(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_LEFT)
cursor_left(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_RIGHT)
cursor_right(editor, 1);
if (event.key_type == KEY_CHAR &&
((event.c >= 'a' && event.c <= 'z') ||
(event.c >= 'A' && event.c <= 'Z') ||
(event.c >= '0' && event.c <= '9') || event.c == ' ' || event.c == '!' ||
event.c == '@' || event.c == '#' || event.c == '$' || event.c == '%' ||
event.c == '^' || event.c == '&' || event.c == '*' || event.c == '(' ||
event.c == ')' || event.c == '-' || event.c == '_' || event.c == '=' ||
event.c == '+' || event.c == '[' || event.c == ']' || event.c == '{' ||
event.c == '}' || event.c == '\\' || event.c == '|' || event.c == ';' ||
event.c == ':' || event.c == '\'' || event.c == '"' || event.c == ',' ||
event.c == '.' || event.c == '<' || event.c == '>' || event.c == '/' ||
event.c == '?' || event.c == '`' || event.c == '~')) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, &event.c, 1);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 1,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 1},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 1);
}
if (event.key_type == KEY_CHAR && event.c == '\t') {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, (char *)" ", 2);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 2,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 2},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 2);
}
if (event.key_type == KEY_CHAR && (event.c == '\n' || event.c == '\r')) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, (char *)"\n", 1);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 1,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 1},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 1);
}
if (event.key_type == KEY_CHAR && event.c == 0x7F) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
TSPoint old_point = {editor->cursor.row, editor->cursor.col};
cursor_left(editor, 1);
uint32_t start = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = erase(editor->root, start, pos - start);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = start,
.old_end_byte = pos,
.new_end_byte = start,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = old_point,
.new_end_point = {editor->cursor.row, editor->cursor.col},
};
editor->edit_queue.push(edit);
}
}
ensure_scroll(editor);
}
int main(int argc, char *argv[]) {
Coord screen = start_screen();
const char *filename = (argc > 1) ? argv[1] : "ts.cpp";
Editor *editor = new_editor(filename, {0, 0}, {screen.row, screen.col});
if (!editor) {
end_screen();
fprintf(stderr, "Failed to load editor\n");
return 1;
}
start_time = std::chrono::high_resolution_clock::now();
std::thread input_thread(input_listener);
std::thread work_thread(background_worker, editor);
while (running) {
render_frames++;
KeyEvent event;
while (event_queue.pop(event))
handle_editor_event(editor, event);
render_editor(editor);
render();
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
input_thread.detach();
if (work_thread.joinable())
work_thread.join();
auto end_time = std::chrono::high_resolution_clock::now();
double seconds = std::chrono::duration<double>(end_time - start_time).count();
double render_fps = render_frames / seconds;
double worker_fps = worker_frames / seconds;
end_screen();
std::cout << "\n======= Performance Summary =======\n";
std::cout << "Runtime: " << seconds << "s\n";
std::cout << "Render loop FPS: " << render_fps << "Hz\n";
std::cout << "Worker loop FPS: " << worker_fps << "Hz\n";
std::cout << "===================================\n";
free_editor(editor);
return 0;
}

View File

@@ -1,26 +1,29 @@
// includes
#include "../../libs/libgrapheme/grapheme.h"
#include "../../libs/unicode_width/unicode_width.h"
#include "../headers/header.hpp"
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
#include "../libs/unicode_width/unicode_width.h"
}
#include "../include/ui.h"
struct termios orig_termios;
termios orig_termios;
int rows, cols;
bool show_cursor = false;
uint32_t rows, cols;
int show_cursor = 0;
std::vector<ScreenCell> screen;
std::vector<ScreenCell> old_screen;
std::mutex screen_mutex;
int real_width(std::string str) {
if (!str.size())
int display_width(const char *str) {
if (!str)
return 0;
if (!strlen(str))
return 0;
const char *p = str.c_str();
if (str[0] == '\t')
return 4;
unicode_width_state_t state;
unicode_width_init(&state);
int width = 0;
for (size_t j = 0; j < str.size(); j++) {
for (size_t j = 0; j < strlen(str); j++) {
unsigned char c = str[j];
if (c < 128) {
int char_width = unicode_width_process(&state, c);
@@ -28,7 +31,7 @@ int real_width(std::string str) {
width += char_width;
} else {
uint_least32_t cp;
size_t bytes = grapheme_decode_utf8(p + j, str.size() - j, &cp);
size_t bytes = grapheme_decode_utf8(str + j, strlen(str) - j, &cp);
if (bytes > 1) {
int char_width = unicode_width_process(&state, cp);
if (char_width > 0)
@@ -37,6 +40,7 @@ int real_width(std::string str) {
}
}
}
return width;
}
@@ -63,6 +67,7 @@ void enable_raw_mode() {
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_lflag |= ISIG;
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
@@ -74,7 +79,7 @@ void enable_raw_mode() {
}
void disable_raw_mode() {
std::string os = "\x1b[?1049l\x1b[?25h";
std::string os = "\x1b[?1049l\x1b[2 q\x1b[?1002l\x1b[?25h";
write(STDOUT_FILENO, os.c_str(), os.size());
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) {
perror("tcsetattr");
@@ -82,30 +87,40 @@ void disable_raw_mode() {
}
}
void start_screen() {
Coord start_screen() {
enable_raw_mode();
get_terminal_size();
screen.assign(rows * cols, {}); // allocate & zero-init
old_screen.assign(rows * cols, {}); // allocate & zero-init
return {rows, cols};
}
void end_screen() { disable_raw_mode(); }
void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg,
uint8_t flags) {
if (row < 0 || row >= rows || col < 0 || col >= cols)
Coord get_size() { return {rows, cols}; }
void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg,
uint32_t bg, uint8_t flags) {
if (row >= rows || col >= cols)
return;
int idx = row * cols + col;
uint32_t idx = row * cols + col;
std::lock_guard<std::mutex> lock(screen_mutex);
screen[idx].utf8 = utf8 ? utf8 : ""; // nullptr => empty string
screen[idx].utf8 = utf8 ? utf8 : "";
screen[idx].fg = fg;
screen[idx].bg = bg;
screen[idx].flags = flags;
}
coords get_size() { return {rows, cols}; }
bool ends_with(const std::string &string_to_check) {
size_t len = string_to_check.size();
if (len < 3)
return false;
return (string_to_check[len - 3] == VS16_BYTE_A) &&
(string_to_check[len - 2] == VS16_BYTE_B) &&
(string_to_check[len - 1] == VS16_BYTE_C);
}
void render() {
static bool first_render = true;
@@ -114,147 +129,135 @@ void render() {
bool current_italic = false;
bool current_bold = false;
bool current_underline = false;
std::lock_guard<std::mutex> lock(screen_mutex);
std::string out;
// reserve a conservative amount to avoid repeated reallocs
out.reserve(static_cast<size_t>(rows) * static_cast<size_t>(cols) * 4 + 256);
// save cursor + hide
out += "\x1b[s\x1b[?25l";
if (first_render) {
out += "\x1b[2J\x1b[H";
first_render = false;
}
for (int row = 0; row < rows; ++row) {
for (uint32_t row = 0; row < rows; ++row) {
int first_change_col = -1;
int last_change_col = -1;
// detect change span in this row
for (int col = 0; col < cols; ++col) {
int idx = row * cols + col;
for (uint32_t col = 0; col < cols; ++col) {
uint32_t idx = row * cols + col;
ScreenCell &old_cell = old_screen[idx];
ScreenCell &new_cell = screen[idx];
bool old_empty = old_cell.utf8.empty();
bool new_empty = new_cell.utf8.empty();
bool content_changed =
(old_empty && !new_empty) || (!old_empty && new_empty) ||
(!old_empty && !new_empty && old_cell.utf8 != new_cell.utf8);
bool content_changed = old_cell.utf8 != new_cell.utf8;
bool style_changed =
(old_cell.fg != new_cell.fg) || (old_cell.bg != new_cell.bg) ||
((old_cell.flags & CF_ITALIC) != (new_cell.flags & CF_ITALIC)) ||
((old_cell.flags & CF_BOLD) != (new_cell.flags & CF_BOLD)) ||
((old_cell.flags & CF_UNDERLINE) != (new_cell.flags & CF_UNDERLINE));
if (content_changed || style_changed) {
if (first_change_col == -1)
first_change_col = col;
last_change_col = col;
}
}
if (first_change_col == -1)
continue;
// move cursor once to the start of change region
char buf[32];
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1,
first_change_col + 1);
out.append(buf, n);
// render changed region
char buf[64];
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, first_change_col + 1);
out.append(buf);
for (int col = first_change_col; col <= last_change_col; ++col) {
int idx = row * cols + col;
ScreenCell &old_cell = old_screen[idx];
ScreenCell &new_cell = screen[idx];
// foreground change
if (current_fg != new_cell.fg) {
if (new_cell.fg) {
char fb[64];
int m = snprintf(
fb, sizeof(fb), "\x1b[38;2;%d;%d;%dm", (new_cell.fg >> 16) & 0xFF,
(new_cell.fg >> 8) & 0xFF, (new_cell.fg >> 0) & 0xFF);
out.append(fb, m);
snprintf(fb, sizeof(fb), "\x1b[38;2;%d;%d;%dm",
(new_cell.fg >> 16) & 0xFF, (new_cell.fg >> 8) & 0xFF,
(new_cell.fg >> 0) & 0xFF);
out.append(fb);
} else {
out += "\x1b[39m";
}
current_fg = new_cell.fg;
}
// background change
if (current_bg != new_cell.bg) {
if (new_cell.bg) {
char bb[64];
int m = snprintf(
bb, sizeof(bb), "\x1b[48;2;%d;%d;%dm", (new_cell.bg >> 16) & 0xFF,
(new_cell.bg >> 8) & 0xFF, (new_cell.bg >> 0) & 0xFF);
out.append(bb, m);
snprintf(bb, sizeof(bb), "\x1b[48;2;%d;%d;%dm",
(new_cell.bg >> 16) & 0xFF, (new_cell.bg >> 8) & 0xFF,
(new_cell.bg >> 0) & 0xFF);
out.append(bb);
} else {
out += "\x1b[49m";
}
current_bg = new_cell.bg;
}
// italic
bool italic = (new_cell.flags & CF_ITALIC) != 0;
if (italic != current_italic) {
out += italic ? "\x1b[3m" : "\x1b[23m";
current_italic = italic;
}
// bold
bool bold = (new_cell.flags & CF_BOLD) != 0;
if (bold != current_bold) {
out += bold ? "\x1b[1m" : "\x1b[22m";
current_bold = bold;
}
// underline
bool underline = (new_cell.flags & CF_UNDERLINE) != 0;
if (underline != current_underline) {
out += underline ? "\x1b[4m" : "\x1b[24m";
current_underline = underline;
}
// content
if (!new_cell.utf8.empty()) {
if (new_cell.utf8[0] == '\t')
if (new_cell.utf8[0] == '\t') {
out.append(" ");
else
out.append(new_cell.utf8);
} else {
// HACK: This is a hack to work around the fact that emojis should be
// double width but handling them as so requires a lot of
// calculations for word wrapping so eventually have to do that
// and render them as the 2 wide they should be.
if (new_cell.utf8.size() > 1) {
if (new_cell.utf8.size() >= 3 && ends_with(new_cell.utf8)) {
out.append(new_cell.utf8.substr(0, new_cell.utf8.size() - 3));
out.append("\xEF\xB8\x8E");
} else {
out.append(new_cell.utf8);
out.append("\xEF\xB8\x8E");
}
} else {
out.append(new_cell.utf8);
}
}
} else {
out.append(1, ' ');
}
// copy new -> old (std::string assignment, no strdup/free)
old_cell.utf8 = new_cell.utf8;
old_cell.fg = new_cell.fg;
old_cell.bg = new_cell.bg;
old_cell.flags = new_cell.flags;
}
}
// final reset + restore cursor + show cursor
out += "\x1b[0m";
out += "\x1b[u";
if (show_cursor)
out += "\x1b[?25h";
// single syscall to write the whole frame
ssize_t written = write(STDOUT_FILENO, out.data(), out.size());
(void)written; // you may check for errors in debug builds
const char *ptr = out.data();
size_t remaining = out.size();
while (remaining > 0) {
ssize_t written = write(STDOUT_FILENO, ptr, remaining);
if (written == 0) {
break;
} else if (written == -1) {
if (errno == EINTR)
continue;
die("write failed");
break;
} else {
ptr += written;
remaining -= written;
}
}
}
void set_cursor(int row, int col, bool show_cursor_param) {
void set_cursor(int row, int col, int show_cursor_param) {
char buf[32];
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, col + 1);
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH\x1b[5 q", row + 1, col + 1);
show_cursor = show_cursor_param;
write(STDOUT_FILENO, buf, n);
}

858
src/rope.cc Normal file
View File

@@ -0,0 +1,858 @@
#include "../include/rope.h"
#include <assert.h>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <pcre2.h>
#include <stdio.h>
#include <string.h>
static void update(Knot *n) {
if (!n)
return;
if (!n->depth || n->depth == 0)
return;
uint32_t left_chars = n->left ? n->left->char_count : 0;
uint32_t right_chars = n->right ? n->right->char_count : 0;
n->char_count = left_chars + right_chars;
uint32_t left_lines = n->left ? n->left->line_count : 0;
uint32_t right_lines = n->right ? n->right->line_count : 0;
n->line_count = left_lines + right_lines;
uint8_t left_depth = n->left ? n->left->depth : 0;
uint8_t right_depth = n->right ? n->right->depth : 0;
n->depth = MAX(left_depth, right_depth) + 1;
n->chunk_size = n->left ? n->left->chunk_size : n->right->chunk_size;
}
// str is not consumed and \0 is not handled
// So if str is null terminated then len must be strlen(str)
// and freed by caller
Knot *load(char *str, uint32_t len, uint32_t chunk_size) {
if (len > (uint32_t)(chunk_size - (chunk_size / 16))) {
Knot *left = load(str, len / 2, chunk_size);
Knot *right = load(str + len / 2, len - len / 2, chunk_size);
Knot *node = (Knot *)malloc(sizeof(Knot));
if (!node)
return nullptr;
node->left = left;
node->right = right;
node->chunk_size = chunk_size;
node->depth = MAX(left->depth, right->depth) + 1;
node->char_count = left->char_count + right->char_count;
node->line_count = left->line_count + right->line_count;
return node;
} else {
Knot *node = (Knot *)malloc(sizeof(Knot) + chunk_size);
if (!node)
return nullptr;
node->left = nullptr;
node->right = nullptr;
node->chunk_size = chunk_size;
node->depth = 0;
node->char_count = len;
uint32_t newline_count = 0;
for (uint32_t i = 0; i < len; i++) {
char c = str[i];
node->data[i] = c;
if (c == '\n')
newline_count++;
}
node->line_count = newline_count;
return node;
}
}
// leaf if consumed and freed (so dont use or free it after)
// left and right are the new nodes
static void split_leaf(Knot *leaf, uint32_t k, Knot **left, Knot **right) {
Knot *left_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
left_node->left = nullptr;
left_node->right = nullptr;
left_node->chunk_size = leaf->chunk_size;
left_node->depth = 0;
left_node->char_count = k;
uint32_t newline_count = 0;
for (uint32_t i = 0; i < k; i++) {
char c = leaf->data[i];
left_node->data[i] = c;
if (c == '\n')
newline_count++;
}
left_node->line_count = newline_count;
uint16_t right_line_count = leaf->line_count - newline_count;
*left = left_node;
Knot *right_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
right_node->left = nullptr;
right_node->right = nullptr;
right_node->chunk_size = leaf->chunk_size;
right_node->depth = 0;
right_node->char_count = leaf->char_count - k;
right_node->line_count = right_line_count;
for (uint32_t i = k; i < leaf->char_count; i++) {
char c = leaf->data[i];
right_node->data[i - k] = c;
}
*right = right_node;
free(leaf);
}
// This makes node nonsensical, so dont use or free it after
void split(Knot *node, uint32_t offset, Knot **left, Knot **right) {
if (!node) {
*left = nullptr;
*right = nullptr;
return;
}
if (node->depth == 0) {
split_leaf(node, offset, left, right);
return;
}
uint32_t left_size = node->left ? node->left->char_count : 0;
if (offset < left_size) {
Knot *L = nullptr, *R = nullptr;
split(node->left, offset, &L, &R);
node->left = R;
update(node);
*right = node;
*left = L;
} else {
uint32_t new_offset = offset - left_size;
Knot *L = nullptr, *R = nullptr;
split(node->right, new_offset, &L, &R);
node->right = L;
update(node);
*left = node;
*right = R;
}
}
static inline int get_balance_factor(Knot *n) {
if (!n)
return 0;
return (int)DEPTH(n->left) - (int)DEPTH(n->right);
}
static inline Knot *rotate_right(Knot *y) {
Knot *x = y->left;
Knot *T2 = x->right;
x->right = y;
y->left = T2;
update(y);
update(x);
return x;
}
static inline Knot *rotate_left(Knot *x) {
Knot *y = x->right;
Knot *T2 = y->left;
y->left = x;
x->right = T2;
update(x);
update(y);
return y;
}
// Technically n can be used after calling
// but use return value instead
Knot *balance(Knot *n) {
update(n);
int bal = get_balance_factor(n);
if (bal > 1) {
if (get_balance_factor(n->left) < 0)
n->left = rotate_left(n->left);
return rotate_right(n);
}
if (bal < -1) {
if (get_balance_factor(n->right) > 0)
n->right = rotate_right(n->right);
return rotate_left(n);
}
return n;
}
// Dont free left or right after calling (only free return value)
// Assumes both ropes have equal chunk sizes
Knot *concat(Knot *left, Knot *right) {
if (!left)
return right;
if (!right)
return left;
if (!left || left->char_count == 0) {
if (left)
free_rope(left);
return right;
}
if (!right || right->char_count == 0) {
if (right)
free_rope(right);
return left;
}
if (left->depth == 0 && right->depth == 0) {
if (left->char_count + right->char_count <= left->chunk_size) {
Knot *node = (Knot *)malloc(sizeof(Knot) + left->chunk_size);
node->left = nullptr;
node->right = nullptr;
node->chunk_size = left->chunk_size;
node->depth = 0;
node->char_count = left->char_count + right->char_count;
node->line_count = left->line_count + right->line_count;
memcpy(node->data, left->data, left->char_count);
memcpy(node->data + left->char_count, right->data, right->char_count);
free(left);
free(right);
return node;
}
}
uint16_t d_left = left->depth;
uint16_t d_right = right->depth;
if (d_left > d_right + 1) {
left->right = concat(left->right, right);
return balance(left);
}
if (d_right > d_left + 1) {
right->left = concat(left, right->left);
return balance(right);
}
Knot *node = (Knot *)malloc(sizeof(Knot));
if (!node)
return nullptr;
node->left = left;
node->right = right;
node->chunk_size = left->chunk_size;
node->depth = MAX(d_left, d_right) + 1;
update(node);
return node;
}
// This makes node nonsensical, so dont use or free it after
// Instead, free the return value or use it in node's place
Knot *insert(Knot *node, uint32_t offset, char *str, uint32_t len) {
if (!node)
return nullptr;
if (len == 0)
return node;
if (node->depth == 0 && node->char_count + len <= node->chunk_size) {
if (offset < node->char_count)
memmove(node->data + offset + len, node->data + offset,
node->char_count - offset);
memcpy(node->data + offset, str, len);
node->char_count += len;
for (uint32_t i = 0; i < len; i++)
if (str[i] == '\n')
node->line_count++;
return node;
}
if (node->depth > 0) {
uint32_t left_count = node->left ? node->left->char_count : 0;
if (offset < left_count) {
Knot *new_left = insert(node->left, offset, str, len);
node->left = new_left;
update(node);
return balance(node);
} else {
Knot *new_right = insert(node->right, offset - left_count, str, len);
node->right = new_right;
update(node);
return balance(node);
}
}
Knot *left_part = nullptr;
Knot *right_part = nullptr;
split(node, offset, &left_part, &right_part);
Knot *middle_part = load(str, len, node->chunk_size);
return concat(concat(left_part, middle_part), right_part);
}
// This makes node nonsensical, so dont use or free it after
// Instead, free the return value or use it in node's place
Knot *erase(Knot *node, uint32_t offset, uint32_t len) {
if (!node || len == 0 || offset >= node->char_count)
return node;
if (offset + len > node->char_count)
len = node->char_count - offset;
if (node->depth == 0) {
uint32_t deleted_newlines = 0;
for (uint32_t i = offset; i < offset + len; i++)
if (node->data[i] == '\n')
deleted_newlines++;
node->line_count -= deleted_newlines;
if (offset + len < node->char_count)
memmove(node->data + offset, node->data + offset + len,
node->char_count - (offset + len));
node->char_count -= len;
return node;
}
uint32_t left_count = node->left ? node->left->char_count : 0;
if (offset + len <= left_count) {
node->left = erase(node->left, offset, len);
} else if (offset >= left_count) {
node->right = erase(node->right, offset - left_count, len);
} else {
Knot *left = nullptr, *middle = nullptr, *right = nullptr;
split(node, offset, &left, &right);
split(right, len, &middle, &right);
free_rope(middle);
return concat(left, right);
}
update(node);
return balance(node);
}
static void _read_into(Knot *node, uint32_t offset, uint32_t len, char *dest) {
if (!node || len == 0)
return;
if (node->depth == 0) {
memcpy(dest, node->data + offset, len);
return;
}
Knot *left = node->left;
uint32_t left_count = left ? left->char_count : 0;
if (offset < left_count) {
uint32_t chunk_len = left_count - offset;
if (chunk_len > len)
chunk_len = len;
_read_into(left, offset, chunk_len, dest);
dest += chunk_len;
len -= chunk_len;
offset = 0;
} else {
offset -= left_count;
}
if (len > 0 && node->right)
_read_into(node->right, offset, len, dest);
}
char *read(Knot *root, uint32_t offset, uint32_t len) {
if (!root)
return nullptr;
if (offset >= root->char_count) {
char *empty = (char *)malloc(1);
if (empty)
empty[0] = '\0';
return empty;
}
if (offset + len > root->char_count) {
len = root->char_count - offset;
}
char *buffer = (char *)malloc((len + 1) * sizeof(char));
if (!buffer)
return nullptr;
_read_into(root, offset, len, buffer);
buffer[len] = '\0';
return buffer;
}
// Hopefully free the tree only once at the end of its use using the pointer
// from the last insert or concat or erase call.
// (or use twice if last call was split - for both left and right).
void free_rope(Knot *root) {
if (!root)
return;
free_rope(root->left);
free_rope(root->right);
free(root);
}
static uint32_t find_nth_newline_offset(Knot *node, uint32_t n) {
if (!node || n > node->line_count)
return UINT32_MAX;
if (node->depth == 0) {
uint32_t count = 0;
for (uint32_t i = 0; i < node->char_count; i++) {
if (node->data[i] == '\n') {
if (count == n)
return i;
count++;
}
}
return UINT32_MAX;
}
uint32_t left_lines = node->left ? node->left->line_count : 0;
if (n < left_lines) {
return find_nth_newline_offset(node->left, n);
} else {
uint32_t right_offset =
find_nth_newline_offset(node->right, n - left_lines);
if (right_offset == UINT32_MAX)
return UINT32_MAX;
uint32_t left_chars = node->left ? node->left->char_count : 0;
return left_chars + right_offset;
}
}
uint32_t byte_to_line(Knot *node, uint32_t offset) {
if (!node)
return 0;
if (offset >= node->char_count)
return node->line_count;
if (node->depth == 0) {
uint32_t lines_before = 0;
uint32_t limit = (offset < node->char_count) ? offset : node->char_count;
for (uint32_t i = 0; i < limit; i++)
if (node->data[i] == '\n')
lines_before++;
return lines_before;
}
uint32_t left_chars = node->left ? node->left->char_count : 0;
if (offset < left_chars) {
return byte_to_line(node->left, offset);
} else {
uint32_t left_lines = node->left ? node->left->line_count : 0;
return left_lines + byte_to_line(node->right, offset - left_chars);
}
}
uint32_t line_to_byte(Knot *node, uint32_t line, uint32_t *out_len) {
if (!node) {
if (out_len)
*out_len = 0;
return 0;
}
uint32_t start_offset = 0;
uint32_t end_offset = 0;
if (line == 0) {
start_offset = 0;
} else {
uint32_t prev_newline = find_nth_newline_offset(node, line - 1);
if (prev_newline == UINT32_MAX)
start_offset = node->char_count;
else
start_offset = prev_newline + 1;
}
uint32_t current_newline = find_nth_newline_offset(node, line);
if (current_newline == UINT32_MAX)
end_offset = node->char_count;
else
end_offset = current_newline + 1;
if (out_len) {
if (end_offset > start_offset)
*out_len = end_offset - start_offset;
else
*out_len = 0;
}
return start_offset;
}
LineIterator *begin_l_iter(Knot *root, uint32_t start_line) {
if (!root)
return nullptr;
if (start_line > root->line_count)
return nullptr;
LineIterator *it = (LineIterator *)malloc(sizeof(LineIterator));
if (!it)
return nullptr;
it->top = 0;
it->line = start_line;
it->node = nullptr;
if (start_line == 0) {
it->offset = 0;
while (root->left) {
it->stack[it->top++] = root;
root = root->left;
}
if (!root->left && !root->right)
it->node = root;
it->stack[it->top++] = root;
return it;
}
Knot *curr = root;
uint32_t relative_line = --start_line;
it->line = start_line;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
break;
}
uint32_t left_lines = (curr->left) ? curr->left->line_count : 0;
if (relative_line < left_lines) {
curr = curr->left;
} else {
relative_line -= left_lines;
curr = curr->right;
}
}
if (!it->node) {
free(it);
return nullptr;
}
it->offset = 0;
if (relative_line > 0) {
uint32_t found_newlines = 0;
uint32_t i = 0;
for (i = 0; i < it->node->char_count; i++) {
if (it->node->data[i] == '\n') {
found_newlines++;
if (found_newlines == relative_line) {
it->offset = i + 1;
break;
}
}
}
}
free(next_line(it));
return it;
}
static inline void iter_advance_leaf(LineIterator *it) {
if (it->top == 0) {
it->node = nullptr;
return;
}
Knot *prev = it->stack[--it->top];
while (it->top > 0) {
Knot *parent = it->stack[it->top - 1];
if (parent->left == prev && parent->right) {
Knot *curr = parent->right;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
it->offset = 0;
return;
}
curr = (curr->left) ? curr->left : curr->right;
}
}
prev = it->stack[--it->top];
}
it->node = nullptr;
}
char *next_line(LineIterator *it) {
if (!it || !it->node)
return nullptr;
size_t capacity = 128;
size_t len = 0;
char *buffer = (char *)malloc(capacity);
if (!buffer)
return nullptr;
while (it->node) {
if (it->offset >= it->node->char_count) {
iter_advance_leaf(it);
if (!it->node)
break;
}
char *start = it->node->data + it->offset;
char *end = it->node->data + it->node->char_count;
char *newline_ptr = (char *)memchr(start, '\n', end - start);
size_t chunk_len;
bool found_newline = false;
if (newline_ptr) {
chunk_len = (newline_ptr - start) + 1;
found_newline = true;
} else {
chunk_len = end - start;
}
if (len + chunk_len + 1 > capacity) {
capacity = (capacity * 2) + chunk_len;
char *new_buf = (char *)realloc(buffer, capacity);
if (!new_buf) {
free(buffer);
return nullptr;
}
buffer = new_buf;
}
memcpy(buffer + len, start, chunk_len);
len += chunk_len;
it->offset += chunk_len;
if (found_newline) {
buffer[len] = '\0';
it->line++;
return buffer;
}
}
if (len > 0) {
buffer[len] = '\0';
it->line++;
return buffer;
}
free(buffer);
return nullptr;
}
LeafIterator *begin_k_iter(Knot *root) {
if (!root)
return nullptr;
LeafIterator *it = (LeafIterator *)malloc(sizeof(LeafIterator));
if (!it)
return nullptr;
it->top = 0;
Knot *curr = root;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
return it;
}
curr = curr->left;
if (!curr) {
curr = it->stack[--it->top]->right;
Knot *temp = it->stack[it->top];
it->stack[it->top++] = temp;
curr = temp->left ? temp->left : temp->right;
Knot *parent = it->stack[it->top - 1];
curr = parent->left;
if (!curr) {
curr = parent->right;
}
}
}
free(it);
return nullptr;
}
// Caller must never free the returned string
char *next_leaf(LeafIterator *it) {
if (!it || !it->node)
return nullptr;
char *data_to_return = it->node->data;
data_to_return[it->node->char_count] = '\0';
Knot *prev_leaf = it->node;
Knot *parent = nullptr;
while (it->top > 0) {
parent = it->stack[--it->top];
if (parent->right && parent->right != prev_leaf) {
Knot *curr = parent->right;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
return data_to_return;
}
curr = curr->left;
if (!curr)
curr = it->stack[it->top - 1]->right;
}
}
prev_leaf = parent;
}
it->node = nullptr;
return data_to_return;
}
ByteIterator *begin_b_iter(Knot *root) {
ByteIterator *b_it = (ByteIterator *)malloc(sizeof(ByteIterator));
LeafIterator *l_it = begin_k_iter(root);
b_it->it = l_it;
b_it->offset_g = 0;
b_it->offset_l = 0;
b_it->char_count = 0;
b_it->data = nullptr;
return b_it;
}
char next_byte(ByteIterator *it) {
if (it->data && it->offset_l < it->char_count) {
return it->data[it->offset_l++];
} else {
it->offset_g += it->offset_l;
it->offset_l = 1;
char *data = next_leaf(it->it);
it->char_count = strlen(data);
it->data = data;
if (it->data)
return *it->data;
else
return '\0';
}
}
std::vector<std::pair<size_t, size_t>> search_rope(Knot *root,
const char *pattern) {
std::vector<std::pair<size_t, size_t>> results;
int errorcode;
PCRE2_SIZE erroffset;
pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, 0,
&errorcode, &erroffset, nullptr);
if (!re) {
fprintf(stderr, "PCRE2 compile error: %d\n", errorcode);
return results;
}
pcre2_match_data *mdata = pcre2_match_data_create(128, nullptr);
int workspace[PCRE_WORKSPACE_SIZE];
LeafIterator *it = begin_k_iter(root);
if (!it) {
pcre2_code_free(re);
pcre2_match_data_free(mdata);
return results;
}
size_t chunk_abs_offset = 0;
size_t saved_match_start = 0;
bool match_in_progress = false;
int flags = PCRE2_PARTIAL_SOFT;
while (1) {
const char *chunk_start = next_leaf(it);
if (!chunk_start)
break;
size_t chunk_len = strlen(chunk_start);
const char *current_ptr = chunk_start;
size_t remaining_len = chunk_len;
while (remaining_len > 0) {
int rc =
pcre2_dfa_match(re, (PCRE2_SPTR)current_ptr, remaining_len, 0, flags,
mdata, nullptr, workspace, PCRE_WORKSPACE_SIZE);
if (rc >= 0) {
PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata);
size_t match_start_abs;
size_t match_end_abs;
if (match_in_progress) {
match_start_abs = saved_match_start;
match_end_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[1];
} else {
match_start_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[0];
match_end_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[1];
}
size_t total_len = match_end_abs - match_start_abs;
results.push_back(std::make_pair(match_start_abs, total_len));
size_t consumed = ov[1];
if (consumed == 0)
consumed = 1;
current_ptr += consumed;
if (consumed > remaining_len)
remaining_len = 0;
else
remaining_len -= consumed;
match_in_progress = false;
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
continue;
} else if (rc == PCRE2_ERROR_PARTIAL) {
PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata);
if (!match_in_progress) {
saved_match_start =
chunk_abs_offset + (current_ptr - chunk_start) + ov[0];
match_in_progress = true;
}
flags |= PCRE2_DFA_RESTART;
flags |= PCRE2_NOTBOL;
break;
} else {
if (match_in_progress) {
match_in_progress = false;
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
current_ptr++;
remaining_len--;
} else {
break;
}
// if (rc != PCRE2_ERROR_NOMATCH) {} // handle error
}
}
chunk_abs_offset += chunk_len;
if (!match_in_progress)
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
}
pcre2_match_data_free(mdata);
pcre2_code_free(re);
free(it);
return results;
}
uint32_t optimal_chunk_size(uint64_t length) {
if (length <= MIN_CHUNK_SIZE)
return MIN_CHUNK_SIZE;
double target_exponent = MIN(std::log2((double)MAX_CHUNK_SIZE),
7.0 + (std::log2((double)length) - 10.0) * 0.25);
uint32_t final_chunk_size =
MAX((uint32_t)MIN_CHUNK_SIZE, (uint32_t)std::pow(2.0, target_exponent));
final_chunk_size = MIN(final_chunk_size, (uint32_t)MAX_CHUNK_SIZE);
final_chunk_size = 1U << (32 - __builtin_clz(final_chunk_size - 1));
return final_chunk_size;
}
// Basic correctness test & usage example
/*
int _main() {
char *buffer = (char *)malloc(44 * 4 + 5);
strcpy(buffer, "The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.");
// This loads all (excluding \0 put in by strcpy)
Knot *root = load(buffer, 44 * 4 + 3, optimal_chunk_size(44 * 4 + 3));
Knot *left = nullptr, *right = nullptr;
// Splits root into left and right (root is no longer valid)
split(root, 5, &left, &right);
// simple read based on byte offset and length
char *s1 = read(left, 0, 100);
printf("%s\n:\n", s1);
char *s2 = read(right, 0, 100);
printf("%s\n;\n", s2);
free(s1);
free(s2);
// Recombines left and right into root (both can
// be valid or invalid in optimized cases)
// they are to not be used after concat
root = concat(left, right);
// root should be set to return value from insert always
root = insert(root, 5, buffer, 5);
free(buffer);
char *s3 = read(root, 0, 100);
printf("%s\n,\n", s3);
// Similar to insert but for erase
root = erase(root, 5, 5);
char *s4 = read(root, 0, 100);
printf("%s\n.\n", s4);
free(s3);
free(s4);
uint32_t byte_offset;
uint32_t len;
// Byte offset given reltive to how it would
// be in a file offset + len includes the \n
// at the end of the line (or nothing is EOF)
byte_offset = line_to_byte(root, 2, &len);
char *s5 = read(root, byte_offset, len);
printf("%s\n'\n", s5);
free(s5);
// returns line number of which line that
// byte position would be in.
// the ending \n position is included in this
uint32_t line = byte_to_line(root, byte_offset + len - 1);
printf("%u\n:\n", line);
// From second line onwards (0 indexed)
LineIterator *it = begin_l_iter(root, 0);
char *c = nullptr;
while ((c = next_line(it)) != nullptr) {
printf("%s :wow:\n", c);
free(c);
}
free(it);
printf("\n/\n");
// Starts at first byte (to be used for regex search)
ByteIterator *it2 = begin_b_iter(root);
uint32_t saved[40];
for (int i = 0; i < 40; i++)
saved[i] = 0;
// std::string pattern = "f.x";
// Inst *program = compile_regex(pattern);
//
// bool result;
// while ((result = next_match(program, it2, saved))) {
// printf("\nRES: %d\n", result);
// for (int i = 0; i < 40; i++)
// printf("%d, ", saved[i]);
// }
// char c2 = ' ';
// while ((c2 = next_byte(it2)) != '\0')
// printf("%c :wow!:\n", c2);
// free(it2);
// search // uses leaf iterator internally // PCRE2 based
std::vector<std::pair<size_t, size_t>> matches = search_rope(root, "f.x");
for (size_t i = 0; i < matches.size(); i++)
printf("\n%lu %lu", matches[i].first, matches[i].second);
// A rope needs to be freed only once if last action on the rope is
// insert or concat or erase.
// for splits we need to free both left and right separately
free_rope(root);
return 0;
}
*/

View File

@@ -1,70 +0,0 @@
class Buffer
# Simple structs for clarity
Diagnostic = Struct.new(:x0, :y0, :x1, :y1, :message)
Highlight = Struct.new(:x0, :y0, :x1, :y1, :fg, :bg)
VirtualText = Struct.new(:x, :y, :lines)
Cursor = Struct.new(:x, :y)
attr_accessor :text, :cursor, :selection_start,
:diagnostics, :highlights, :virtual_texts
def initialize(initial_text = "")
@text = initial_text
@cursor = Cursor.new(0, 0)
@selection_start = Cursor.new(0, 0)
@diagnostics = []
@highlights = []
@virtual_texts = []
end
# Utility methods
def lines
@text.split("\n")
end
def line_count
lines.size
end
def line(y)
lines[y] || ""
end
def insert(x, y, str)
current_line = lines[y] || ""
before = current_line[0...x] || ""
after = current_line[x..-1] || ""
lines_arr = lines
lines_arr[y] = before + str + after
@text = lines_arr.join("\n")
end
def erase(x0, y0, x1, y1)
lines_arr = lines
if y0 == y1
line = lines_arr[y0] || ""
lines_arr[y0] = line[0...x0] + line[x1..-1].to_s
else
first = lines_arr[y0][0...x0]
last = lines_arr[y1][x1..-1].to_s
lines_arr[y0..y1] = [first + last]
end
@text = lines_arr.join("\n")
end
# Add overlays
def add_diagnostic(x0, y0, x1, y1, message)
@diagnostics << Diagnostic.new(x0, y0, x1, y1, message)
end
def add_highlight(x0, y0, x1, y1, fg: 0xFFFFFF, bg: 0x000000)
@highlights << Highlight.new(x0, y0, x1, y1, fg, bg)
end
def add_virtual_text(x, y, lines)
@virtual_texts << VirtualText.new(x, y, lines)
end
def render()
end
end

View File

@@ -1,57 +0,0 @@
require "ffi"
require_relative "utils"
module C
extend FFI::Library
ffi_lib File.join(__dir__, "../../builds/C-crib.so")
class KeyEvent < FFI::Struct
layout :key_type, :uint8,
:c, :char,
:special_key, :uint8,
:special_modifier, :uint8,
:mouse_x, :uint8,
:mouse_y, :uint8,
:mouse_button, :uint8,
:mouse_state, :uint8,
:mouse_direction, :uint8,
:mouse_modifier, :uint8
def to_s
case KEY_TYPE[self[:key_type]]
when :char
"#<KeyEvent char=#{self[:c].inspect}>"
when :special
"#<KeyEvent special key=#{SPECIAL_KEY[self[:special_key]]} mod=#{MODIFIER[self[:special_modifier]]}>"
when :mouse
"#<KeyEvent mouse x=#{self[:mouse_x]} y=#{self[:mouse_y]} " \
"btn=#{MOUSE_BUTTON[self[:mouse_button]]} state=#{MOUSE_STATE[self[:mouse_state]]} " \
"scroll_dir=#{SCROLL_DIR[self[:mouse_direction]]} mod=#{MODIFIER[self[:mouse_modifier]]}>"
else
"#<KeyEvent type=#{self[:key_type]} unknown=true>"
end
end
end
class Coords < FFI::Struct
layout :x, :int,
:y, :int
def to_ary
[self[:x], self[:y]]
end
def to_s
"#<Coords x=#{self[:x]} y=#{self[:y]}>"
end
end
attach_function :start_screen, [], :void
attach_function :end_screen, [], :void
attach_function :update, [:int, :int, :string, :uint32, :uint32, :uint8], :void
attach_function :render, [], :void
attach_function :set_cursor, [:int, :int, :int], :void
attach_function :read_key, [], KeyEvent.by_value
attach_function :get_size, [], Coords.by_value
attach_function :real_width, [:string], :int
end

View File

@@ -1,56 +0,0 @@
def ctrl_key(k)
k.ord & 0x1F
end
# Key types
KEY_TYPE = {
0 => :char,
1 => :special,
2 => :mouse
}
# Special keys
SPECIAL_KEY = {
0 => :up,
1 => :down,
2 => :left,
3 => :right,
4 => :delete
}
# Control key
KEY_ESC = "\x1b"
# Mouse states
MOUSE_STATE = {
0 => :press,
1 => :release,
2 => :drag,
3 => :scroll
}
# Mouse buttons
MOUSE_BUTTON = {
0 => :left,
1 => :middle,
2 => :right,
3 => :scroll,
4 => :none
}
# Scroll directions
SCROLL_DIR = {
0 => :up,
1 => :down,
2 => :left,
3 => :right,
4 => :none
}
# Modifiers
MODIFIER = {
1 => :alt,
2 => :cntrl,
3 => :cntrl_alt,
4 => :shift
}

243
src/ts.cc Normal file
View File

@@ -0,0 +1,243 @@
#include "../include/ts.h"
#include "../include/editor.h"
#include "../include/rope.h"
#include <cstdint>
#include <fstream>
#include <regex>
#include <string>
#include <unordered_map>
static std::unordered_map<std::string, std::regex> regex_cache;
static const std::regex scm_regex(
R"((@[A-Za-z0-9_.]+)|(;; \#[0-9a-fA-F]{6} \#[0-9a-fA-F]{6} [01] [01] [01] \d+))");
TSQuery *load_query(const char *query_path, Editor *editor) {
const TSLanguage *lang = editor->language;
std::ifstream file(query_path, std::ios::in | std::ios::binary);
if (!file.is_open())
return nullptr;
std::string highlight_query((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
std::smatch match;
std::map<std::string, int> capture_name_cache;
Highlight *c_hl = nullptr;
int i = 0;
int limit = 20;
editor->query_map.resize(limit);
std::string::const_iterator searchStart(highlight_query.cbegin());
while (std::regex_search(searchStart, highlight_query.cend(), match,
scm_regex)) {
std::string mct = match.str();
if (mct.substr(0, 1) == "@") {
std::string capture_name = mct;
if (!capture_name_cache.count(capture_name)) {
if (c_hl) {
if (i >= limit) {
limit += 20;
editor->query_map.resize(limit);
}
editor->query_map[i] = *c_hl;
delete c_hl;
c_hl = nullptr;
}
capture_name_cache[capture_name] = i;
i++;
}
} else if (mct.substr(0, 2) == ";;") {
if (c_hl)
delete c_hl;
c_hl = new Highlight();
c_hl->fg = HEX(mct.substr(4, 6));
c_hl->bg = HEX(mct.substr(12, 6));
int bold = std::stoi(mct.substr(19, 1));
int italic = std::stoi(mct.substr(21, 1));
int underline = std::stoi(mct.substr(23, 1));
c_hl->priority = std::stoi(mct.substr(25));
c_hl->flags = (bold ? CF_BOLD : 0) | (italic ? CF_ITALIC : 0) |
(underline ? CF_UNDERLINE : 0);
}
searchStart = match.suffix().first;
}
if (c_hl)
delete c_hl;
uint32_t error_offset = 0;
TSQueryError error_type = (TSQueryError)0;
TSQuery *q = ts_query_new(lang, highlight_query.c_str(),
(uint32_t)highlight_query.length(), &error_offset,
&error_type);
return q;
}
static inline const TSNode *find_capture_node(const TSQueryMatch &match,
uint32_t capture_id) {
for (uint32_t i = 0; i < match.capture_count; i++)
if (match.captures[i].index == capture_id)
return &match.captures[i].node;
return nullptr;
}
static inline std::string node_text(const TSNode &node, Knot *source) {
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
char *text = read(source, start, end - start);
std::string final = std::string(text, end - start);
free(text);
return final;
}
static inline bool ts_predicate(TSQuery *query, const TSQueryMatch &match,
Knot *source) {
uint32_t step_count;
const TSQueryPredicateStep *steps =
ts_query_predicates_for_pattern(query, match.pattern_index, &step_count);
if (!steps || step_count != 4)
return true;
if (source->char_count >= (64 * 1024))
return false;
std::string command;
std::string regex_txt;
uint32_t subject_id = 0;
for (uint32_t i = 0; i < step_count; i++) {
const TSQueryPredicateStep *step = &steps[i];
if (step->type == TSQueryPredicateStepTypeDone)
break;
switch (step->type) {
case TSQueryPredicateStepTypeString: {
uint32_t length = 0;
const char *s =
ts_query_string_value_for_id(query, step->value_id, &length);
if (i == 0)
command.assign(s, length);
else
regex_txt.assign(s, length);
break;
}
case TSQueryPredicateStepTypeCapture: {
subject_id = step->value_id;
break;
}
case TSQueryPredicateStepTypeDone:
break;
}
}
const TSNode *node = find_capture_node(match, subject_id);
std::string subject = node_text(*node, source);
auto it = regex_cache.find(regex_txt);
if (it == regex_cache.end())
it = regex_cache.emplace(regex_txt, std::regex(regex_txt)).first;
const std::regex &re = it->second;
if (command == "match?")
return std::regex_match(subject, re);
else if (command == "not-match?")
return !std::regex_match(subject, re);
return false;
}
const char *read_ts(void *payload, uint32_t byte_index, TSPoint,
uint32_t *bytes_read) {
if (!running) {
*bytes_read = 0;
return "";
}
TSLoad *load = (TSLoad *)payload;
Knot *root = load->editor->root;
if (load->prev)
free(load->prev);
if (byte_index >= root->char_count) {
*bytes_read = 0;
load->prev = nullptr;
return "";
}
uint32_t chunk_size = 4096;
uint32_t remaining = root->char_count - byte_index;
uint32_t len_to_read = remaining > chunk_size ? chunk_size : remaining;
std::shared_lock lock(load->editor->knot_mtx);
char *buffer = read(root, byte_index, len_to_read);
lock.unlock();
load->prev = buffer;
*bytes_read = len_to_read;
return buffer;
}
static inline Highlight *safe_get(std::vector<Highlight> &vec, size_t index) {
if (index >= vec.size())
return nullptr;
return &vec[index];
}
void ts_collect_spans(Editor *editor) {
if (!editor->parser || !editor->root || !editor->query)
return;
TSLoad load = {editor, nullptr};
TSInput tsinput = {
.payload = &load,
.read = read_ts,
.encoding = TSInputEncodingUTF8,
.decode = nullptr,
};
TSTree *tree, *copy = nullptr;
std::unique_lock lock_0(editor->knot_mtx);
if (editor->tree)
copy = ts_tree_copy(editor->tree);
lock_0.unlock();
if (!running)
return;
std::vector<TSInputEdit> edits;
TSInputEdit edit;
if (copy) {
while (editor->edit_queue.pop(edit)) {
edits.push_back(edit);
ts_tree_edit(copy, &edits.back());
}
}
tree = ts_parser_parse(editor->parser, copy, tsinput);
ts_tree_delete(copy);
while (editor->edit_queue.pop(edit)) {
edits.push_back(edit);
ts_tree_edit(tree, &edits.back());
}
lock_0.lock();
if (editor->tree)
ts_tree_delete(editor->tree);
editor->tree = tree;
copy = ts_tree_copy(tree);
lock_0.unlock();
std::shared_lock lock_1(editor->span_mtx);
TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, editor->query, ts_tree_root_node(copy));
std::vector<Span> new_spans;
new_spans.reserve(4096);
TSQueryMatch match;
while (ts_query_cursor_next_match(cursor, &match)) {
if (!running)
break;
if (!ts_predicate(editor->query, match, editor->root))
continue;
for (uint32_t i = 0; i < match.capture_count; i++) {
if (!running)
break;
TSQueryCapture cap = match.captures[i];
uint32_t start = ts_node_start_byte(cap.node);
uint32_t end = ts_node_end_byte(cap.node);
Highlight *hl = safe_get(editor->query_map, cap.index);
if (hl)
new_spans.push_back({start, end, hl});
}
}
if (!running) {
ts_tree_delete(copy);
ts_query_cursor_delete(cursor);
if (load.prev)
free(load.prev);
return;
}
lock_1.unlock();
std::sort(new_spans.begin(), new_spans.end());
std::unique_lock lock_2(editor->span_mtx);
editor->spans.swap(new_spans);
lock_2.unlock();
ts_query_cursor_delete(cursor);
if (load.prev)
free(load.prev);
}

87
src/utils.cc Normal file
View File

@@ -0,0 +1,87 @@
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
}
#include "../include/utils.h"
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <limits.h>
#include <string.h>
#include <string>
#include <unistd.h>
std::string get_exe_dir() {
char exe_path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", exe_path, PATH_MAX);
if (count == -1)
return "";
exe_path[count] = '\0';
std::string path(exe_path);
return path.substr(0, path.find_last_of('/'));
}
uint32_t grapheme_strlen(const char *s) {
if (!s)
return 0;
uint32_t count = 0;
const char *p = s;
while (*p) {
uint32_t next = grapheme_next_character_break_utf8(p, UINT32_MAX);
if (!next)
next = 1;
p += next;
count++;
}
return count;
}
uint32_t get_visual_col_from_bytes(const char *line, uint32_t byte_limit) {
if (!line)
return 0;
uint32_t visual_col = 0;
uint32_t current_byte = 0;
uint32_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
len--;
while (current_byte < byte_limit && current_byte < len) {
uint32_t inc = grapheme_next_character_break_utf8(line + current_byte,
len - current_byte);
if (current_byte + inc > byte_limit)
break;
current_byte += inc;
visual_col++;
}
return visual_col;
}
uint32_t get_bytes_from_visual_col(const char *line,
uint32_t target_visual_col) {
if (!line)
return 0;
uint32_t visual_col = 0;
uint32_t current_byte = 0;
uint32_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
len--;
while (visual_col < target_visual_col && current_byte < len) {
uint32_t inc = grapheme_next_character_break_utf8(line + current_byte,
len - current_byte);
current_byte += inc;
visual_col++;
}
return current_byte;
}
void log(const char *fmt, ...) {
FILE *fp = fopen("/tmp/log.txt", "a");
if (!fp)
return;
va_list args;
va_start(args, fmt);
vfprintf(fp, fmt, args);
va_end(args);
fputc('\n', fp);
fclose(fp);
}