From 13295a2fcdb3c4075ab89e163f7cbb20b3003ba4 Mon Sep 17 00:00:00 2001 From: Krzosa Karol Date: Thu, 10 Apr 2025 11:01:32 +0200 Subject: [PATCH] text editor --- src/core/core_arena.c | 5 + src/core/core_basic.h | 1 + src/text_editor/core_array.c | 2 +- src/text_editor/text_editor_buffer.c | 368 ++++++++++++++++++++++++++- 4 files changed, 367 insertions(+), 9 deletions(-) diff --git a/src/core/core_arena.c b/src/core/core_arena.c index ec1dfcb..1c6789c 100644 --- a/src/core/core_arena.c +++ b/src/core/core_arena.c @@ -170,3 +170,8 @@ fn ma_temp_t ma_begin_scratch1(ma_arena_t *conflict) { ma_arena_t *conflicts[] = {conflict}; return ma_begin_scratch_ex(conflicts, 1); } + +fn ma_temp_t ma_begin_scratch2(ma_arena_t *a, ma_arena_t *b) { + ma_arena_t *conflicts[] = {a, b}; + return ma_begin_scratch_ex(conflicts, 2); +} diff --git a/src/core/core_basic.h b/src/core/core_basic.h index 17ffac4..defbf9a 100644 --- a/src/core/core_basic.h +++ b/src/core/core_basic.h @@ -115,6 +115,7 @@ union convert_f32_i32_t { f32 f; i32 i; }; #define PASTE_(a, b) a##b #define PASTE(a, b) PASTE_(a, b) #define SWAP(t, a, b) do { t PASTE(temp__, __LINE__) = a; a = b; b = PASTE(temp__, __LINE__); } while(0) +#define SWAP_PTR(t, a, b) do { t PASTE(temp__, __LINE__) = *a; *a = *b; *b = PASTE(temp__, __LINE__); } while(0) #define CODE(...) #__VA_ARGS__ #if PLATFORM_CL || (PLATFORM_CLANG && PLATFORM_WINDOWS) diff --git a/src/text_editor/core_array.c b/src/text_editor/core_array.c index ece17ea..046745c 100644 --- a/src/text_editor/core_array.c +++ b/src/text_editor/core_array.c @@ -45,7 +45,7 @@ typedef array(i64) array_i64_t; #define arrisz(x) sizeof((x)->data[0]) #define arrcst(x) ((array_void_t *)(x)) #define array_init(a, this, count) (array__init((a), arrcst(this), arrisz(this), (count))) -#define array_add(this, item) (array__grow(arrcst(this), arrisz(this), 1), (this)->data[(this)->len++] = (item)) +#define array_add(this, ...) (array__grow(arrcst(this), arrisz(this), 1), (this)->data[(this)->len++] = __VA_ARGS__) #define array_pop(this) (assert_expr((this)->len > 0), (this)->data[--(this)->len]) #define array_dealloc(this) (dealloc((this)->alo, (this)->data)) #define array_addn(this, n) (array__grow(arrcst(this), arrisz(this), (n)), (this)->len += (n), &(this)->data[(this)->len - (n)]) diff --git a/src/text_editor/text_editor_buffer.c b/src/text_editor/text_editor_buffer.c index 7122330..94346f9 100644 --- a/src/text_editor/text_editor_buffer.c +++ b/src/text_editor/text_editor_buffer.c @@ -65,6 +65,10 @@ struct buffer16_t { }; gb i64 buffer_raw_ids; +const b32 dont_kill_selection = false; +const b32 kill_selection = true; + +fn void buffer16_multi_cursor_apply_edits(buffer16_t *buffer, array_edit16_t edits); /////////////////////////////// // buffer helpers @@ -468,8 +472,7 @@ fn void buffer16_save_history_before_apply_edits(buffer16_t *buffer, array_histo array_copy(buffer->alo, &entry->edits, edits); // make reverse edits - for (i64 i = 0; i < edits->len; i += 1) { - edit16_t *it = edits->data + i; + array_for(edit16_t, it, &entry->edits) { it->range = (r1i64_t){it->range.min, it->range.min + it->string.len}; it->string = s16_alo_copy(buffer->alo, buffer16_get_string(buffer, it->range)); } @@ -505,7 +508,7 @@ fn void buffer16_redo_edit(buffer16_t *buffer, array_caret_t *carets) { history16_t entry = array_pop(&buffer->redo_stack); buffer16_save_history_before_merge_cursor(buffer, &buffer->undo_stack, carets); buffer16_save_history_before_apply_edits(buffer, &buffer->undo_stack, &entry.edits); - // buffer16_multi_cursor_apply_edits(buffer, entry.edits); // @todo + buffer16_multi_cursor_apply_edits(buffer, entry.edits); array_dealloc(carets); *carets = entry.carets; @@ -514,15 +517,32 @@ fn void buffer16_redo_edit(buffer16_t *buffer, array_caret_t *carets) { dealloc(buffer->alo, it->string.str); } array_dealloc(&entry.edits); - } -fn void buffer16_apply_edits(buffer16_t *buffer, array_edit16_t *edits) { +fn void buffer16_undo_edit(buffer16_t *buffer, array_caret_t *carets) { + if (!buffer->flags.history || buffer->redo_stack.len <= 0) { + return; + } + + history16_t entry = array_pop(&buffer->undo_stack); + buffer16_save_history_before_merge_cursor(buffer, &buffer->redo_stack, carets); + buffer16_save_history_before_apply_edits(buffer, &buffer->redo_stack, &entry.edits); + buffer16_multi_cursor_apply_edits(buffer, entry.edits); + + array_dealloc(carets); + *carets = entry.carets; + + array_for(edit16_t, it, &entry.edits) { + dealloc(buffer->alo, it->string.str); + } + array_dealloc(&entry.edits); +} + +fn void buffer16__apply_edits(buffer16_t *buffer, array_edit16_t *edits) { assert(buffer->edit_phase == 1); buffer->edit_phase += 1; buffer16_save_history_before_apply_edits(buffer, &buffer->undo_stack, edits); - // buffer16_multi_cursor_apply_edits(buffer, entry.edits); // @todo - // @todo: ... + buffer16_multi_cursor_apply_edits(buffer, *edits); } fn void buffer16_dealloc_history_entries(buffer16_t *buffer, array_history16_t *entries) { @@ -571,6 +591,296 @@ fn void caret_assert_ranges(array_caret_t carets) { } } +fn void buffer16_adjust_carets(array_edit16_t *edits, array_caret_t *carets) { + ma_temp_t scratch = ma_begin_scratch1(edits->alo.object); + assert(edits->alo.object == carets->alo.object); + + array_caret_t new_carets = {0}; + array_copy(ma_temp_alo(scratch), &new_carets, carets); + array_for(edit16_t, it, edits) { + i64 remove_size = r1i64_size(it->range); + i64 insert_size = it->string.len; + i64 offset = insert_size - remove_size; + for (i64 i = 0; i < carets->len; i += 1) { + caret_t *old_caret = carets->data + i; + caret_t *new_caret = new_carets.data + i; + + if (old_caret->range.min > it->range.min) { + new_caret->range.min += offset; + new_caret->range.max += offset; + } + } + } + + for (i64 i = 0; i < carets->len; i += 1) { + carets->data[i] = new_carets.data[i]; + } + + ma_end_scratch(scratch); +} + +fn void buffer16_end_edit(buffer16_t *buffer, array_edit16_t *edits, array_caret_t *carets, b32 kill_selection) { + buffer16__apply_edits(buffer, edits); + + assert(buffer->edit_phase == 2); + buffer->edit_phase -= 1; + +#if BUFFER_DEBUG + if (buffer->flags.history) { + history16_t *entry = &array_last(&buffer->undo_stack); + assert(entry->carets.len); + assert(entry->edits.len); + for (i64 i = 0; i < edits->len - 1; i += 1) { + assert(edits->data[i].range.min <= edits->data[i + 1].range.min); + } + } +#endif + + // Adjust carets + // this one also moves the carets forward if they are aligned with edit + ma_temp_t scratch = ma_begin_scratch1(buffer->alo.object); + assert(buffer->alo.object == carets->alo.object); + assert(buffer->alo.object == edits->alo.object); + + array_caret_t new_carets = {0}; + array_copy(ma_temp_alo(scratch), &new_carets, carets); + array_for(edit16_t, it, edits) { + i64 remove_size = r1i64_size(it->range); + i64 insert_size = it->string.len; + i64 offset = insert_size - remove_size; + + for (i64 i = 0; i < carets->len; i += 1) { + caret_t *old_caret = carets->data + i; + caret_t *new_caret = new_carets.data + i; + + if (old_caret->range.min == it->range.min) { + new_caret->range.min += insert_size; + } else if (old_caret->range.min > it->range.min) { + new_caret->range.min += offset; + } + + if (old_caret->range.max == it->range.max) { + new_caret->range.max += insert_size; + } else if (old_caret->range.max > it->range.max) { + new_caret->range.max += offset; + } + + assert(new_caret->range.max >= new_caret->range.min); + } + } + + for (i64 i = 0; i < carets->len; i += 1) { + carets->data[i] = new_carets.data[i]; + if (kill_selection) { + carets->data[i].range.max = carets->data[i].range.min; + } + } + + ma_end_scratch(scratch); +} + +void caret_merge_sort(i64 Count, caret_t *First, caret_t *Temp) { + // SortKey = range.min + if (Count == 1) { + // NOTE(casey): No work to do. + } else if (Count == 2) { + caret_t *EntryA = First; + caret_t *EntryB = First + 1; + if (EntryA->range.min > EntryB->range.min) { + SWAP_PTR(caret_t, EntryA, EntryB); + } + } else { + i64 Half0 = Count / 2; + i64 Half1 = Count - Half0; + + assert(Half0 >= 1); + assert(Half1 >= 1); + + caret_t *InHalf0 = First; + caret_t *InHalf1 = First + Half0; + caret_t *End = First + Count; + + caret_merge_sort(Half0, InHalf0, Temp); + caret_merge_sort(Half1, InHalf1, Temp); + + caret_t *ReadHalf0 = InHalf0; + caret_t *ReadHalf1 = InHalf1; + + caret_t *Out = Temp; + for (i64 Index = 0; + Index < Count; + ++Index) { + if (ReadHalf0 == InHalf1) { + *Out++ = *ReadHalf1++; + } else if (ReadHalf1 == End) { + *Out++ = *ReadHalf0++; + } else if (ReadHalf0->range.min < ReadHalf1->range.min) { + *Out++ = *ReadHalf0++; + } else { + *Out++ = *ReadHalf1++; + } + } + assert(Out == (Temp + Count)); + assert(ReadHalf0 == InHalf1); + assert(ReadHalf1 == End); + + // TODO(casey): Not really necessary if we ping-pong + for (i64 Index = 0; + Index < Count; + ++Index) { + First[Index] = Temp[Index]; + } + } +} + +void edit16_merge_sort(i64 Count, edit16_t *First, edit16_t *Temp) { + // SortKey = range.min + if (Count == 1) { + // NOTE(casey): No work to do. + } else if (Count == 2) { + edit16_t *EntryA = First; + edit16_t *EntryB = First + 1; + if (EntryA->range.min > EntryB->range.min) { + SWAP_PTR(edit16_t, EntryA, EntryB); + } + } else { + i64 Half0 = Count / 2; + i64 Half1 = Count - Half0; + + assert(Half0 >= 1); + assert(Half1 >= 1); + + edit16_t *InHalf0 = First; + edit16_t *InHalf1 = First + Half0; + edit16_t *End = First + Count; + + edit16_merge_sort(Half0, InHalf0, Temp); + edit16_merge_sort(Half1, InHalf1, Temp); + + edit16_t *ReadHalf0 = InHalf0; + edit16_t *ReadHalf1 = InHalf1; + + edit16_t *Out = Temp; + for (i64 Index = 0; + Index < Count; + ++Index) { + if (ReadHalf0 == InHalf1) { + *Out++ = *ReadHalf1++; + } else if (ReadHalf1 == End) { + *Out++ = *ReadHalf0++; + } else if (ReadHalf0->range.min < ReadHalf1->range.min) { + *Out++ = *ReadHalf0++; + } else { + *Out++ = *ReadHalf1++; + } + } + assert(Out == (Temp + Count)); + assert(ReadHalf0 == InHalf1); + assert(ReadHalf1 == End); + + // TODO(casey): Not really necessary if we ping-pong + for (i64 Index = 0; + Index < Count; + ++Index) { + First[Index] = Temp[Index]; + } + } +} + +fn void buffer16_multi_cursor_apply_edits(buffer16_t *buffer, array_edit16_t edits) { +#if BUFFER_DEBUG + assert(buffer->line_starts.len); + assert(edits.len); + array_for(edit16_t, it, &edits) { + assert(it->range.min >= 0); + assert(it->range.max >= it->range.min); + assert(it->range.max <= buffer->len); + } + + array_for(edit16_t, it1, &edits) { + array_for(edit16_t, it2, &edits) { + if (it1 == it2) continue; + + b32 a2_inside = it2->range.min >= it1->range.min && it2->range.min < it1->range.max; + assert(!a2_inside); + + b32 b2_inside = it2->range.max > it1->range.min && it2->range.max <= it1->range.max; + assert(!b2_inside); + } + } +#endif + + // We need to sort from lowest to highest based on range.min + { + ma_temp_t scratch = ma_begin_scratch1(buffer->alo.object); + array_edit16_t edits_copy = {0}; + array_copy(ma_temp_alo(scratch), &edits_copy, &edits); + if (edits.len > 1) edit16_merge_sort(edits.len, edits_copy.data, edits.data); + edits = edits_copy; + ma_end_scratch(scratch); + } + +#if BUFFER_DEBUG + for (i64 i = 0; i < edits.len - 1; i += 1) { + assert(edits.data[i].range.min <= edits.data[i + 1].range.min); + } +#endif + + // @optimize: we can do all edits in one go with less memory copies probably + // or something else entirely + i64 offset = 0; + array_for(edit16_t, it, &edits) { + it->range.min += offset; + it->range.max += offset; + offset += it->string.len - r1i64_size(it->range); + buffer16_raw_replace_text(buffer, it->range, it->string); + } + +} + +fn void buffer16_add_edit(array_edit16_t *edits, r1i64_t range, s16_t string) { + array_add(edits, (edit16_t){range, string}); +} + +// Merge carets that overlap, this needs to be handled before any edits to +// make sure overlapping edits won't happen. +// +// mouse_selection_anchor is special case for mouse handling ! +fn void buffer16_merge_carets(buffer16_t *buffer, array_caret_t *carets) { + array_for(caret_t, it, carets) { + it->range = buffer16_clamp_range(buffer, it->range); + } + caret_t first_caret = carets->data[0]; + + ma_temp_t scratch = ma_begin_scratch1(buffer->alo.object); + array_caret_t c1 = {0}; + array_copy(ma_temp_alo(scratch), &c1, carets); + if (carets->len > 1) { + caret_merge_sort(carets->len, c1.data, carets->data); + } + carets->len = 0; + + i64 first_caret_index = 0; + array_add(carets, c1.data[0]); + for (i64 i = 1; i < c1.len; i += 1) { + caret_t *it = c1.data + i; + caret_t *last = &array_last(carets); + + if (r1i64_overlap(it->range, last->range)) { + last->range.max = MAX(last->range.max, it->range.max); + } else { + array_add(carets, *it); + } + + if (carets_are_equal(*it, first_caret)) { + first_caret_index = carets->len - 1; + } + } + + SWAP(caret_t, carets->data[first_caret_index], carets->data[0]); + ma_end_scratch(scratch); +} + fn_test void buffer16_test(void) { ma_temp_t scratch = ma_begin_scratch(); @@ -631,7 +941,49 @@ fn_test void buffer16_test(void) { } + { + + buffer16_t *buffer = ma_push_type(scratch.arena, buffer16_t); + buffer16_raw_init(ma_temp_alo(scratch), buffer, S8_FILE_AND_LINE, 128); + buffer16_raw_replace_text(buffer, r1i64_null, s16("some thing or another and stuff like that")); + + { + array_caret_t carets = {0}; + array_init(ma_temp_alo(scratch), &carets, 32); + array_add(&carets, caret_make(0, 4)); + array_add(&carets, caret_make(3, 8)); + array_add(&carets, caret_make(3, 8)); + array_add(&carets, caret_make(3, 8)); + array_add(&carets, caret_make(3, 8)); + array_add(&carets, caret_make(3, 6)); + array_add(&carets, caret_make(3, 6)); + buffer16_merge_carets(buffer, &carets); + assert(carets.len == 1); + assert(carets.data[0].range.min == 0 && carets.data[0].range.max == 8); + } + + { + array_caret_t carets = {0}; + array_init(ma_temp_alo(scratch), &carets, 32); + array_add(&carets, caret_make(0, 4)); + array_add(&carets, caret_make(0, 3)); + array_add(&carets, caret_make(5, 10)); + + array_edit16_t edits = buffer16_begin_edit(ma_temp_alo(scratch), buffer, &carets); + + buffer16_merge_carets(buffer, &carets); + assert(carets.len == 2); + + array_for(caret_t, it, &carets) { + buffer16_add_edit(&edits, it->range, s16("meme")); + } + + buffer16_end_edit(buffer, &edits, &carets, kill_selection); + assert(s16_are_equal(buffer->string, s16("meme meme or another and stuff like that"))); + } + } + ma_end_scratch(scratch); exit(0); -} \ No newline at end of file +}