porting text editor
This commit is contained in:
@@ -10,5 +10,6 @@ void run_all_tests(void) {
|
||||
test_string16();
|
||||
test_hash_table();
|
||||
test_intern_table();
|
||||
test_array();
|
||||
test_arr();
|
||||
buffer16_raw_test();
|
||||
}// run_all_tests()
|
||||
|
||||
480
src/text_editor/text_editor_buffer.c
Normal file
480
src/text_editor/text_editor_buffer.c
Normal file
@@ -0,0 +1,480 @@
|
||||
#define BUFFER_DEBUG 1
|
||||
|
||||
typedef struct caret_t caret_t;
|
||||
struct caret_t {
|
||||
i64 ifront;
|
||||
union {
|
||||
r1i64_t range;
|
||||
i64 pos[2];
|
||||
};
|
||||
};
|
||||
|
||||
typedef struct xy_t xy_t;
|
||||
struct xy_t {
|
||||
i64 col;
|
||||
i64 line;
|
||||
};
|
||||
|
||||
typedef struct edit16_t edit16_t;
|
||||
struct edit16_t {
|
||||
r1i64_t range;
|
||||
s16_t string;
|
||||
};
|
||||
|
||||
// @todo: redo tree
|
||||
typedef struct history16_t history16_t;
|
||||
struct history16_t {
|
||||
edit16_t *edits; // @array
|
||||
caret_t *carets; // @array
|
||||
};
|
||||
|
||||
typedef struct buffer16_id_t buffer16_id_t;
|
||||
struct buffer16_id_t { i64 e; };
|
||||
|
||||
typedef struct buffer16_t buffer16_t;
|
||||
struct buffer16_t {
|
||||
s8_t name;
|
||||
buffer16_id_t id;
|
||||
|
||||
i32 change_id;
|
||||
i8 edit_phase;
|
||||
|
||||
struct {
|
||||
b8 dirty: 1;
|
||||
b8 history: 1;
|
||||
b8 line_starts: 1;
|
||||
} flags;
|
||||
|
||||
union {
|
||||
s16_t string;
|
||||
u16 *data;
|
||||
struct {
|
||||
u16 *str;
|
||||
i64 len;
|
||||
};
|
||||
};
|
||||
i64 cap;
|
||||
i64 *line_starts; // @array
|
||||
|
||||
history16_t *undo_stack; // @array
|
||||
history16_t *redo_stack; // @array
|
||||
ma_arena_t *arena;
|
||||
};
|
||||
|
||||
gb i64 buffer_raw_ids;
|
||||
|
||||
///////////////////////////////
|
||||
// buffer helpers
|
||||
fn i64 buffer16_pos_to_line(buffer16_t *buffer, i64 pos) {
|
||||
// @todo: rewrite this without modifying the line_starts
|
||||
assert(buffer->flags.line_starts);
|
||||
arr_add(buffer->line_starts, buffer->len + 1);
|
||||
|
||||
// binary search
|
||||
i64 low = 0;
|
||||
|
||||
// -2 here because we use 2 indices and combine them into one line range so we
|
||||
// don't want to access last item since that would index past array.
|
||||
i64 high = arr_len(buffer->line_starts) - 2;
|
||||
i64 result = 0;
|
||||
|
||||
while (low <= high) {
|
||||
i64 mid = low + (high - low) / 2;
|
||||
r1i64_t range = {buffer->line_starts[mid], buffer->line_starts[mid + 1]};
|
||||
if (pos >= range.min && pos < range.max) {
|
||||
result = mid;
|
||||
break;
|
||||
}
|
||||
|
||||
if (range.min < pos) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
arr_pop(buffer->line_starts);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_get_line_range_full(buffer16_t *buffer, i64 line, i64 *eof) {
|
||||
assert(line < arr_len(buffer->line_starts));
|
||||
r1i64_t result = {buffer->line_starts[line], buffer->len};
|
||||
if (line + 1 < arr_len(buffer->line_starts)) {
|
||||
result.max = buffer->line_starts[line + 1];
|
||||
} else {
|
||||
*eof = 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_get_line_range(buffer16_t *buffer, i64 line) {
|
||||
i64 eof = 0;
|
||||
r1i64_t line_range = buffer16_get_line_range_full(buffer, line, &eof);
|
||||
line_range.max = line_range.max - 1 + eof;
|
||||
return line_range;
|
||||
}
|
||||
|
||||
fn xy_t buffer16_pos_to_xy(buffer16_t *buffer, i64 pos) {
|
||||
i64 eof = 0;
|
||||
i64 line = buffer16_pos_to_line(buffer, pos);
|
||||
r1i64_t line_range = buffer16_get_line_range_full(buffer, line, &eof);
|
||||
i64 col = pos - line_range.min;
|
||||
xy_t result = {col, line};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 buffer16_xy_to_pos(buffer16_t *buffer, xy_t xy) {
|
||||
xy.line = CLAMP(xy.line, 0, arr_len(buffer->line_starts) - 1);
|
||||
i64 eof = 0;
|
||||
r1i64_t line_range = buffer16_get_line_range_full(buffer, xy.line, &eof);
|
||||
i64 pos = CLAMP(xy.col + line_range.min, line_range.min, line_range.max);
|
||||
return pos;
|
||||
}
|
||||
|
||||
fn i64 buffer16_xy_to_pos_without_new_line(buffer16_t *buffer, xy_t xy) {
|
||||
xy.line = CLAMP(xy.line, 0, arr_len(buffer->line_starts) - 1);
|
||||
i64 eof = 0;
|
||||
r1i64_t line_range = buffer16_get_line_range_full(buffer, xy.line, &eof);
|
||||
i64 pos = CLAMP(xy.col + line_range.min, line_range.min, line_range.max - 1 + eof);
|
||||
return pos;
|
||||
}
|
||||
|
||||
fn s16_t buffer16_get_string(buffer16_t *buffer, r1i64_t range) {
|
||||
range = r1i64_clamp(range, 0, buffer->len);
|
||||
s16_t result = s16_make(buffer->str + range.min, r1i64_size(range));
|
||||
return result;
|
||||
}
|
||||
|
||||
fn s16_t buffer16_get_line_string(buffer16_t *buffer, i64 line) {
|
||||
r1i64_t range = buffer16_get_line_range(buffer, line);
|
||||
s16_t string = buffer16_get_string(buffer, range);
|
||||
return string;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_get_range_end(buffer16_t *buffer) {
|
||||
r1i64_t result = {buffer->len, buffer->len};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_get_range(buffer16_t *buffer) {
|
||||
r1i64_t result = {0, buffer->len};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 buffer16_clamp_pos(buffer16_t *buffer, i64 pos) {
|
||||
i64 result = CLAMP(pos, 0, buffer->len);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_clamp_range(buffer16_t *buffer, r1i64_t range) {
|
||||
r1i64_t result = {buffer16_clamp_pos(buffer, range.min), buffer16_clamp_pos(buffer, range.max)};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 caret_get_front(caret_t caret) {
|
||||
i64 result = caret.pos[caret.ifront];
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 caret_get_back(caret_t caret) {
|
||||
i64 result = caret.pos[(caret.ifront + 1) % 2];
|
||||
return result;
|
||||
}
|
||||
|
||||
fn caret_t caret_make(i64 front, i64 back) {
|
||||
caret_t result = {0};
|
||||
if (front >= back) {
|
||||
result.range.min = back;
|
||||
result.range.max = front;
|
||||
result.ifront = 1;
|
||||
} else {
|
||||
result.range.min = front;
|
||||
result.range.max = back;
|
||||
result.ifront = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn caret_t caret_set_front(caret_t caret, i64 pos) {
|
||||
i64 back = caret_get_back(caret);
|
||||
caret_t result = caret_make(pos, back);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn caret_t caret_set_back(caret_t caret, i64 pos) {
|
||||
i64 front = caret_get_front(caret);
|
||||
caret_t result = caret_make(front, pos);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn b32 carets_are_equal(caret_t a, caret_t b) {
|
||||
b32 result = r1i64_are_equal(a.range, b.range) && a.ifront == b.ifront;
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_word_start(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
for (i64 i = pos - 1; i >= 0; i -= 1) {
|
||||
if (!char16_is_word(buffer->str[i]))
|
||||
break;
|
||||
pos = i;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_word_end(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
for (i64 i = pos;; i += 1) {
|
||||
pos = i;
|
||||
// this is because buffer end terminates the loop
|
||||
// too early and we cannot establish the proper range
|
||||
// semantics - proper max is one past last index
|
||||
if (!(i < buffer->len))
|
||||
break;
|
||||
if (!char16_is_word(buffer->str[i]))
|
||||
break;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_next_word_end(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
u16 prev = 0;
|
||||
for (i64 i = pos;; i += 1) {
|
||||
pos = i;
|
||||
// this is because buffer end terminates the loop
|
||||
// too early and we cannot establish the proper range
|
||||
// semantics - proper max is one past last index
|
||||
if (!(i < buffer->len))
|
||||
break;
|
||||
fn if (prev == L'\n' || (prev && prev != buffer->str[i]) || char16_is_word(buffer->str[i])) {
|
||||
break;
|
||||
}
|
||||
prev = buffer->str[i];
|
||||
}
|
||||
i64 result = prev == L'\n' ? pos : buffer16_get_word_end(buffer, pos);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_prev_word_start(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
u16 prev = 0;
|
||||
i64 i = pos - 1;
|
||||
for (; i >= 0; i -= 1) {
|
||||
fn if (prev == L'\n' || (prev && prev != buffer->str[i]) || char16_is_word(buffer->str[i])) {
|
||||
break;
|
||||
}
|
||||
pos = i;
|
||||
prev = buffer->str[i];
|
||||
}
|
||||
b32 new_line = prev == L'\n';
|
||||
i64 result = new_line ? pos : buffer16_get_word_start(buffer, pos);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_line_start(buffer16_t *buffer, i64 pos) {
|
||||
i64 eof = 0;
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
i64 line = buffer16_pos_to_line(buffer, pos);
|
||||
r1i64_t range = buffer16_get_line_range_full(buffer, line, &eof);
|
||||
return range.min;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_line_end(buffer16_t *buffer, i64 pos) {
|
||||
i64 eof = 0;
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
i64 line = buffer16_pos_to_line(buffer, pos);
|
||||
r1i64_t range = buffer16_get_line_range_full(buffer, line, &eof);
|
||||
return range.max;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_line_start_full(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
i64 line = buffer16_pos_to_line(buffer, pos);
|
||||
r1i64_t range = buffer16_get_line_range(buffer, line);
|
||||
return range.min;
|
||||
}
|
||||
|
||||
fn i64 buffer16_get_line_end_full(buffer16_t *buffer, i64 pos) {
|
||||
pos = CLAMP(pos, (i64)0, buffer->len);
|
||||
i64 line = buffer16_pos_to_line(buffer, pos);
|
||||
r1i64_t range = buffer16_get_line_range(buffer, line);
|
||||
return range.max;
|
||||
}
|
||||
|
||||
fn r1i64_t buffer16_enclose_word(buffer16_t *buffer, i64 pos) {
|
||||
r1i64_t result = {buffer16_get_word_start(buffer, pos), buffer16_get_word_end(buffer, pos)};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn u16 buffer16_get_char(buffer16_t *buffer, i64 pos) {
|
||||
if (pos >= 0 && pos < buffer->len) {
|
||||
return buffer->str[pos];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
// raw buffer operations
|
||||
fn void buffer16_raw_init(ma_arena_t *arena, buffer16_t *buffer, s8_t name, i64 size) {
|
||||
buffer->id = (buffer16_id_t){++buffer_raw_ids};
|
||||
buffer->name = name;
|
||||
buffer->arena = arena;
|
||||
buffer->cap = size;
|
||||
buffer->data = ma_push_array(arena, u16, size);
|
||||
buffer->line_starts = arr_create(arena, i64, 128);
|
||||
buffer->undo_stack = arr_create(arena, history16_t, 128);
|
||||
buffer->redo_stack = arr_create(arena, history16_t, 128);
|
||||
buffer->flags.line_starts = true;
|
||||
buffer->flags.history = true;
|
||||
arr_add(buffer->line_starts, 0);
|
||||
}
|
||||
|
||||
fn void buffer16_raw_grow(buffer16_t *buffer, i64 change_size) {
|
||||
// @todo: add memory block management, also for arrays
|
||||
i64 new_size = buffer->len + change_size;
|
||||
if (new_size > buffer->cap) {
|
||||
// Allocator alo = buffer->line_starts.allocator;
|
||||
i64 outside = new_size - buffer->cap;
|
||||
i64 new_cap = (buffer->cap + outside) * 2;
|
||||
u16 *new_array = ma_push_array(buffer->arena, u16, new_cap);
|
||||
memory_copy(new_array, buffer->data, buffer->len * sizeof(u16));
|
||||
// Dealloc(alo, &buffer->data);
|
||||
buffer->cap = new_cap;
|
||||
buffer->data = new_array;
|
||||
}
|
||||
}
|
||||
|
||||
fn void buffer16_raw_offset_all_lines_forward(buffer16_t *buffer, i64 line, i64 *_offset) {
|
||||
i64 offset = *_offset;
|
||||
*_offset = 0;
|
||||
if (offset == 0) return;
|
||||
|
||||
for (i64 i = line; i < arr_len(buffer->line_starts); i += 1) {
|
||||
buffer->line_starts[i] += offset;
|
||||
}
|
||||
}
|
||||
|
||||
fn void buffer16_raw_update_lines(buffer16_t *buffer, r1i64_t range, s16_t string) {
|
||||
if (buffer->flags.line_starts == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
i64 min_line_number = buffer16_pos_to_line(buffer, range.min);
|
||||
assert(min_line_number < arr_len(buffer->line_starts));
|
||||
// Update lines remove
|
||||
{
|
||||
i64 line_offset = 0;
|
||||
i64 lines_to_remove = min_line_number + 1;
|
||||
i64 lines_to_remove_count = 0;
|
||||
for (i64 i = range.min; i < range.max; i += 1) {
|
||||
u16 c = buffer->data[i];
|
||||
if (c == '\n') {
|
||||
lines_to_remove_count += 1;
|
||||
}
|
||||
line_offset -= 1;
|
||||
}
|
||||
arr_deln(buffer->line_starts, lines_to_remove, lines_to_remove_count);
|
||||
buffer16_raw_offset_all_lines_forward(buffer, min_line_number + 1, &line_offset);
|
||||
}
|
||||
|
||||
// Update lines add
|
||||
i64 line_offset = 0;
|
||||
i64 nl = min_line_number + 1;
|
||||
for (i64 i = 0; i < string.len; i += 1) {
|
||||
nl = min_line_number + 1;
|
||||
b32 next_line_valid = nl < arr_len(buffer->line_starts);
|
||||
|
||||
if (string.str[i] == L'\n') {
|
||||
i64 new_line_pos = range.min + i + 1;
|
||||
line_offset += 1;
|
||||
|
||||
arr_insert(buffer->line_starts, nl, new_line_pos);
|
||||
buffer16_raw_offset_all_lines_forward(buffer, nl + 1, &line_offset);
|
||||
min_line_number = nl;
|
||||
} else if (next_line_valid) {
|
||||
line_offset += 1;
|
||||
}
|
||||
}
|
||||
buffer16_raw_offset_all_lines_forward(buffer, nl, &line_offset);
|
||||
}
|
||||
|
||||
fn void buffer16_raw_validate_line_starts(buffer16_t *buffer) {
|
||||
i64 line = 0;
|
||||
for (i64 i = 0; i < buffer->len; i += 1) {
|
||||
i64 l = buffer16_pos_to_line(buffer, i);
|
||||
assert(l == line);
|
||||
if (buffer->data[i] == L'\n') line += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn void buffer16_raw_replace_text(buffer16_t *buffer, r1i64_t range, s16_t string) {
|
||||
assert(range.max >= range.min);
|
||||
assert(range.max >= 0 && range.max <= buffer->len);
|
||||
assert(range.min >= 0 && range.min <= buffer->len);
|
||||
buffer->flags.dirty = true;
|
||||
buffer->change_id += 1;
|
||||
|
||||
i64 size_to_remove = range.max - range.min;
|
||||
i64 size_to_add = string.len;
|
||||
i64 change_size = size_to_add - size_to_remove;
|
||||
assert(change_size + buffer->len >= 0);
|
||||
buffer16_raw_grow(buffer, change_size);
|
||||
|
||||
i64 range_size = range.max - range.min;
|
||||
u16 *begin_remove = buffer->data + range.min;
|
||||
u16 *end_remove = begin_remove + range_size;
|
||||
i64 remain_len = buffer->len - (range.min + range_size);
|
||||
|
||||
buffer16_raw_update_lines(buffer, range, string);
|
||||
|
||||
u16 *begin_add = begin_remove;
|
||||
u16 *end_add = begin_add + string.len;
|
||||
memory_move(end_add, end_remove, remain_len * sizeof(u16));
|
||||
memory_copy(begin_add, string.str, string.len * sizeof(u16));
|
||||
buffer->len = buffer->len + change_size;
|
||||
|
||||
#if BUFFER_DEBUG
|
||||
buffer16_raw_validate_line_starts(buffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
fn_test void buffer16_raw_test(void) {
|
||||
ma_temp_t scratch = ma_begin_scratch();
|
||||
|
||||
s16_t string = s16("thing itself");
|
||||
buffer16_t *buffer = ma_push_type(scratch.arena, buffer16_t);
|
||||
buffer16_raw_init(scratch.arena, buffer, S8_FILE_AND_LINE, 16);
|
||||
buffer16_raw_replace_text(buffer, r1i64_null, string);
|
||||
assert(s16_are_equal(buffer->string, string));
|
||||
assert(buffer->cap == 16);
|
||||
assert(arr_len(buffer->line_starts) == 1);
|
||||
|
||||
buffer16_raw_replace_text(buffer, r1i64(5, 6), s16("|||"));
|
||||
assert(s16_are_equal(buffer->string, s16("thing|||itself")));
|
||||
assert(arr_len(buffer->line_starts) == 1);
|
||||
|
||||
buffer16_raw_replace_text(buffer, buffer16_get_range_end(buffer), s16("|||MEMES|||AndStuff"));
|
||||
assert(s16_are_equal(buffer->string, s16("thing|||itself|||MEMES|||AndStuff")));
|
||||
assert(buffer->cap > 16);
|
||||
assert(arr_len(buffer->line_starts) == 1);
|
||||
|
||||
buffer16_raw_replace_text(buffer, buffer16_get_range_end(buffer), s16("\nnext line"));
|
||||
assert(arr_len(buffer->line_starts) == 2);
|
||||
|
||||
buffer16_raw_replace_text(buffer, r1i64(4, 4), s16("\nnext line"));
|
||||
buffer16_raw_replace_text(buffer, r1i64(20, 20), s16("\nnext line"));
|
||||
buffer16_raw_replace_text(buffer, r1i64(14, 15), s16("\nnext line"));
|
||||
buffer16_raw_replace_text(buffer, r1i64(0, 0), s16("\nnext line"));
|
||||
buffer16_raw_replace_text(buffer, r1i64(9, 10), s16("\nnext line"));
|
||||
assert(arr_len(buffer->line_starts) == 7);
|
||||
|
||||
buffer16_raw_replace_text(buffer, r1i64(8, 12), s16_null);
|
||||
assert(arr_len(buffer->line_starts) == 6);
|
||||
|
||||
|
||||
ma_end_scratch(scratch);
|
||||
|
||||
exit(0);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
#include "core/core.h"
|
||||
#include "os/os.h"
|
||||
#include "app/app.h"
|
||||
#include "profiler/profiler.h"
|
||||
// #include "ui/ui.h"
|
||||
|
||||
|
||||
#include "core/core.c"
|
||||
#include "os/os.c"
|
||||
#include "app/app.c"
|
||||
#include "profiler/profiler.c"
|
||||
#include "render/render.c"
|
||||
#include "text_editor_buffer.c"
|
||||
// #include "ui/ui.c"
|
||||
|
||||
#include "text_editor.gen.c"
|
||||
@@ -21,42 +25,6 @@
|
||||
* Win32 upload icon
|
||||
**/
|
||||
|
||||
typedef struct caret_t caret_t;
|
||||
struct caret_t {
|
||||
i64 ifront;
|
||||
union {
|
||||
r1i64_t range;
|
||||
i64 pos[2];
|
||||
};
|
||||
};
|
||||
|
||||
typedef struct xy_t xy_t;
|
||||
struct xy_t {
|
||||
i64 col;
|
||||
i64 line;
|
||||
};
|
||||
|
||||
typedef struct buffer_id_t buffer_id_t;
|
||||
struct buffer_id_t { i64 e; };
|
||||
|
||||
typedef struct buffer_t buffer_t;
|
||||
struct buffer_t {
|
||||
buffer_id_t id;
|
||||
|
||||
union {
|
||||
s16_t string;
|
||||
struct {
|
||||
u16 *data;
|
||||
i64 len;
|
||||
};
|
||||
};
|
||||
i64 cap;
|
||||
|
||||
i64 *line_starts;
|
||||
i64 line_count;
|
||||
};
|
||||
|
||||
|
||||
fn_export b32 app_update(thread_ctx_t *thread_ctx, app_frame_t *frame) {
|
||||
tcx = thread_ctx;
|
||||
if (frame->first_event->kind == app_event_kind_init) {
|
||||
|
||||
Reference in New Issue
Block a user