diff options
Diffstat (limited to 'src/buffers.c')
| -rw-r--r-- | src/buffers.c | 701 |
1 files changed, 701 insertions, 0 deletions
diff --git a/src/buffers.c b/src/buffers.c new file mode 100644 index 0000000..e8c1d23 --- /dev/null +++ b/src/buffers.c @@ -0,0 +1,701 @@ +#include "buffers.h" + +#include "common.h" +#include "util.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#define BUFFER_INITIAL_CAP 64 + +static int buffer_is_word_char(char c) { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_'; +} + +static int buffer_require_editable(buffer_t *buffer) { + if (!buffer || buffer->read_only) return ECEX_ERR; + return ECEX_OK; +} + +static void buffer_undo_entry_free(ecex_undo_entry_t *entry) { + if (!entry) return; + free(entry->data); + entry->data = NULL; + entry->len = 0; + entry->point = 0; + entry->mark = 0; + entry->mark_active = 0; +} + +static void buffer_undo_stack_clear(ecex_undo_entry_t *stack, size_t count) { + if (!stack) return; + for (size_t i = 0; i < count; i++) buffer_undo_entry_free(&stack[i]); +} + +static int buffer_push_snapshot_to(ecex_undo_entry_t **stack, + size_t *count, + size_t *cap, + buffer_t *buffer) { + if (!stack || !count || !cap || !buffer) return ECEX_ERR; + if (ECEX_GROW_ARRAY(*stack, *count, *cap, 64) != ECEX_OK) return ECEX_ERR; + ecex_undo_entry_t *entry = &(*stack)[(*count)++]; + memset(entry, 0, sizeof(*entry)); + entry->data = malloc(buffer->len + 1); + if (!entry->data) { (*count)--; return ECEX_ERR; } + memcpy(entry->data, buffer->data, buffer->len + 1); + entry->len = buffer->len; + entry->point = buffer->point; + entry->mark = buffer->mark; + entry->mark_active = buffer->mark_active; + return ECEX_OK; +} + +static void buffer_clear_redo(buffer_t *buffer) { + if (!buffer) return; + buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); + buffer->redo_count = 0; +} + +static int buffer_record_undo(buffer_t *buffer) { + if (!buffer || buffer->undo_disabled) return ECEX_OK; + if (buffer_push_snapshot_to(&buffer->undo_stack, + &buffer->undo_count, + &buffer->undo_cap, + buffer) != ECEX_OK) { + return ECEX_ERR; + } + buffer_clear_redo(buffer); + return ECEX_OK; +} + +static int buffer_restore_snapshot(buffer_t *buffer, ecex_undo_entry_t *entry) { + if (!buffer || !entry || !entry->data) return ECEX_ERR; + char *copy = malloc(entry->len + 1); + if (!copy) return ECEX_ERR; + memcpy(copy, entry->data, entry->len + 1); + free(buffer->data); + buffer->data = copy; + buffer->len = entry->len; + buffer->cap = entry->len + 1; + buffer->point = ECEX_MIN(entry->point, buffer->len); + buffer->mark = ECEX_MIN(entry->mark, buffer->len); + buffer->mark_active = entry->mark_active; + buffer->modified = 1; + return ECEX_OK; +} + +buffer_t *buffer_new(const char *name, const char *path, int read_only) { + assert(name && "buffer name cannot be NULL"); + + buffer_t *buffer = calloc(1, sizeof(*buffer)); + if (!buffer) return NULL; + + buffer->name = ecex_strdup(name); + buffer->path = ecex_strdup(path); + buffer->data = malloc(1); + + if (!buffer->name || (path && !buffer->path) || !buffer->data) { + buffer_free(buffer); + return NULL; + } + + buffer->data[0] = '\0'; + buffer->len = 0; + buffer->cap = 1; + buffer->read_only = read_only; + + return buffer; +} + +void buffer_free(buffer_t *buffer) { + if (!buffer) return; + + buffer_clear_interactive_actions(buffer); + buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); + buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); + free(buffer->undo_stack); + free(buffer->redo_stack); + free(buffer->interactive_actions); + free(buffer->name); + free(buffer->path); + free(buffer->data); + free(buffer); +} + +int buffer_reserve(buffer_t *buffer, size_t needed) { + ECEX_RETURN_ERR_IF_NULL(buffer); + if (needed <= buffer->cap) return ECEX_OK; + + size_t new_cap = buffer->cap ? buffer->cap : BUFFER_INITIAL_CAP; + while (new_cap < needed) { + new_cap *= 2; + } + + char *new_data = realloc(buffer->data, new_cap); + if (!new_data) return ECEX_ERR; + + buffer->data = new_data; + buffer->cap = new_cap; + return ECEX_OK; +} + +int buffer_clear(buffer_t *buffer) { + if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; + + if (buffer_reserve(buffer, 1) != ECEX_OK) return ECEX_ERR; + + buffer->data[0] = '\0'; + buffer->len = 0; + buffer->point = 0; + buffer->mark = 0; + buffer->mark_active = 0; + buffer->scroll_line = 0; + buffer->scroll_col = 0; + buffer_clear_interactive_actions(buffer); + buffer->modified = 1; + return ECEX_OK; +} + +int buffer_set_text(buffer_t *buffer, const char *text) { + if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR; + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; + + size_t len = strlen(text); + if (buffer_reserve(buffer, len + 1) != ECEX_OK) return ECEX_ERR; + + memcpy(buffer->data, text, len + 1); + buffer->len = len; + buffer->point = len; + buffer->mark = 0; + buffer->mark_active = 0; + buffer->scroll_line = 0; + buffer->scroll_col = 0; + buffer_clear_interactive_actions(buffer); + buffer->modified = 1; + return ECEX_OK; +} + +int buffer_insert_at(buffer_t *buffer, size_t pos, const char *text) { + if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR; + if (pos > buffer->len) return ECEX_ERR; + + size_t text_len = strlen(text); + if (text_len == 0) return ECEX_OK; + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; + size_t needed = buffer->len + text_len + 1; + + if (buffer_reserve(buffer, needed) != ECEX_OK) return ECEX_ERR; + + memmove(buffer->data + pos + text_len, + buffer->data + pos, + buffer->len - pos + 1); + memcpy(buffer->data + pos, text, text_len); + + buffer->len += text_len; + buffer->point = pos + text_len; + buffer->modified = 1; + return ECEX_OK; +} + +int buffer_insert(buffer_t *buffer, const char *text) { + ECEX_RETURN_ERR_IF_NULL(buffer); + return buffer_insert_at(buffer, buffer->point, text); +} + +int buffer_append(buffer_t *buffer, const char *text) { + ECEX_RETURN_ERR_IF_NULL(buffer); + return buffer_insert_at(buffer, buffer->len, text); +} + +int buffer_prepend(buffer_t *buffer, const char *text) { + return buffer_insert_at(buffer, 0, text); +} + +int buffer_insert_char_at(buffer_t *buffer, size_t pos, char c) { + if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; + if (pos > buffer->len) return ECEX_ERR; + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; + + if (buffer_reserve(buffer, buffer->len + 2) != ECEX_OK) return ECEX_ERR; + + memmove(buffer->data + pos + 1, + buffer->data + pos, + buffer->len - pos + 1); + + buffer->data[pos] = c; + buffer->len++; + buffer->point = pos + 1; + buffer->modified = 1; + return ECEX_OK; +} + +int buffer_insert_char(buffer_t *buffer, char c) { + ECEX_RETURN_ERR_IF_NULL(buffer); + return buffer_insert_char_at(buffer, buffer->point, c); +} + +int buffer_delete_range(buffer_t *buffer, size_t start, size_t end) { + if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; + if (start > end || end > buffer->len) return ECEX_ERR; + if (start == end) return ECEX_OK; + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; + + size_t deleted = end - start; + + memmove(buffer->data + start, + buffer->data + end, + buffer->len - end + 1); + + buffer->len -= deleted; + + if (buffer->point > end) { + buffer->point -= deleted; + } else if (buffer->point > start) { + buffer->point = start; + } + + if (buffer->mark > end) { + buffer->mark -= deleted; + } else if (buffer->mark > start) { + buffer->mark = start; + } + + if (buffer->mark_active && buffer->mark == buffer->point) { + buffer->mark_active = 0; + } + + buffer->modified = 1; + return ECEX_OK; +} + +int buffer_delete_selection(buffer_t *buffer) { + if (!buffer_has_selection(buffer)) return ECEX_OK; + + size_t start = 0; + size_t end = 0; + buffer_selection_range(buffer, &start, &end); + + int result = buffer_delete_range(buffer, start, end); + if (result == ECEX_OK) buffer_clear_mark(buffer); + return result; +} + +int buffer_replace_selection(buffer_t *buffer, const char *text) { + if (!buffer || !text) return ECEX_ERR; + if (!buffer_has_selection(buffer)) return buffer_insert(buffer, text); + + size_t start = 0; + size_t end = 0; + buffer_selection_range(buffer, &start, &end); + + if (buffer_delete_range(buffer, start, end) != ECEX_OK) return ECEX_ERR; + buffer->point = start; + buffer_clear_mark(buffer); + return buffer_insert(buffer, text); +} + +int buffer_backspace(buffer_t *buffer) { + if (!buffer) return ECEX_ERR; + if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer); + if (buffer->point == 0) return ECEX_OK; + return buffer_delete_range(buffer, buffer->point - 1, buffer->point); +} + +int buffer_delete_forward(buffer_t *buffer) { + if (!buffer) return ECEX_ERR; + if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer); + if (buffer->point >= buffer->len) return ECEX_OK; + return buffer_delete_range(buffer, buffer->point, buffer->point + 1); +} + +int buffer_kill_line(buffer_t *buffer) { + if (!buffer) return ECEX_ERR; + + size_t end = buffer_line_end_at(buffer, buffer->point); + if (end == buffer->point && end < buffer->len && buffer->data[end] == '\n') { + end++; + } + + return buffer_delete_range(buffer, buffer->point, end); +} + +void buffer_set_point(buffer_t *buffer, size_t point) { + if (!buffer) return; + buffer->point = ECEX_MIN(point, buffer->len); +} + +void buffer_move_left(buffer_t *buffer) { + if (!buffer || buffer->point == 0) return; + buffer->point--; +} + +void buffer_move_right(buffer_t *buffer) { + if (!buffer || buffer->point >= buffer->len) return; + buffer->point++; +} + +void buffer_move_up(buffer_t *buffer) { + if (!buffer) return; + + size_t current_start = buffer_line_start_at(buffer, buffer->point); + if (current_start == 0) { + buffer->point = 0; + return; + } + + size_t wanted_col = buffer->point - current_start; + size_t prev_end = current_start - 1; + size_t prev_start = buffer_line_start_at(buffer, prev_end); + size_t prev_len = prev_end - prev_start; + + buffer->point = prev_start + ECEX_MIN(wanted_col, prev_len); +} + +void buffer_move_down(buffer_t *buffer) { + if (!buffer) return; + + size_t current_start = buffer_line_start_at(buffer, buffer->point); + size_t current_end = buffer_line_end_at(buffer, buffer->point); + if (current_end >= buffer->len) { + buffer->point = buffer->len; + return; + } + + size_t wanted_col = buffer->point - current_start; + size_t next_start = current_end + 1; + size_t next_end = buffer_line_end_at(buffer, next_start); + size_t next_len = next_end - next_start; + + buffer->point = next_start + ECEX_MIN(wanted_col, next_len); +} + +void buffer_move_word_left(buffer_t *buffer) { + if (!buffer) return; + + size_t pos = buffer->point; + while (pos > 0 && !buffer_is_word_char(buffer->data[pos - 1])) pos--; + while (pos > 0 && buffer_is_word_char(buffer->data[pos - 1])) pos--; + buffer->point = pos; +} + +void buffer_move_word_right(buffer_t *buffer) { + if (!buffer) return; + + size_t pos = buffer->point; + while (pos < buffer->len && !buffer_is_word_char(buffer->data[pos])) pos++; + while (pos < buffer->len && buffer_is_word_char(buffer->data[pos])) pos++; + buffer->point = pos; +} + +void buffer_move_beginning_of_line(buffer_t *buffer) { + if (!buffer) return; + buffer->point = buffer_line_start_at(buffer, buffer->point); +} + +void buffer_move_end_of_line(buffer_t *buffer) { + if (!buffer) return; + buffer->point = buffer_line_end_at(buffer, buffer->point); +} + +void buffer_move_beginning_of_buffer(buffer_t *buffer) { + if (!buffer) return; + buffer->point = 0; +} + +void buffer_move_end_of_buffer(buffer_t *buffer) { + if (!buffer) return; + buffer->point = buffer->len; +} + +void buffer_set_mark(buffer_t *buffer, size_t mark) { + if (!buffer) return; + buffer->mark = ECEX_MIN(mark, buffer->len); + buffer->mark_active = 1; +} + +void buffer_clear_mark(buffer_t *buffer) { + if (!buffer) return; + buffer->mark_active = 0; +} + +int buffer_has_selection(buffer_t *buffer) { + return buffer && buffer->mark_active && buffer->mark != buffer->point; +} + +void buffer_selection_range(buffer_t *buffer, size_t *out_start, size_t *out_end) { + size_t start = 0; + size_t end = 0; + + if (buffer && buffer_has_selection(buffer)) { + start = ECEX_MIN(buffer->point, buffer->mark); + end = ECEX_MAX(buffer->point, buffer->mark); + } + + if (out_start) *out_start = start; + if (out_end) *out_end = end; +} + +size_t buffer_line_start_at(buffer_t *buffer, size_t pos) { + if (!buffer) return 0; + pos = ECEX_MIN(pos, buffer->len); + + while (pos > 0 && buffer->data[pos - 1] != '\n') { + pos--; + } + + return pos; +} + +size_t buffer_line_end_at(buffer_t *buffer, size_t pos) { + if (!buffer) return 0; + pos = ECEX_MIN(pos, buffer->len); + + while (pos < buffer->len && buffer->data[pos] != '\n') { + pos++; + } + + return pos; +} + +size_t buffer_current_line_start(buffer_t *buffer) { + if (!buffer) return 0; + return buffer_line_start_at(buffer, buffer->point); +} + +size_t buffer_current_line_end(buffer_t *buffer) { + if (!buffer) return 0; + return buffer_line_end_at(buffer, buffer->point); +} + +size_t buffer_current_column(buffer_t *buffer) { + if (!buffer) return 0; + return buffer->point - buffer_line_start_at(buffer, buffer->point); +} + +size_t buffer_current_line_number(buffer_t *buffer) { + if (!buffer) return 0; + + size_t line = 1; + for (size_t i = 0; i < buffer->point && i < buffer->len; i++) { + if (buffer->data[i] == '\n') line++; + } + + return line; +} + +size_t buffer_line_count(buffer_t *buffer) { + if (!buffer || buffer->len == 0) return 1; + + size_t lines = 1; + for (size_t i = 0; i < buffer->len; i++) { + if (buffer->data[i] == '\n') lines++; + } + + return lines; +} + +char *buffer_substring(buffer_t *buffer, size_t start, size_t end) { + if (!buffer || start > end || end > buffer->len) return NULL; + + size_t len = end - start; + char *copy = malloc(len + 1); + if (!copy) return NULL; + + memcpy(copy, buffer->data + start, len); + copy[len] = '\0'; + return copy; +} + +char *buffer_current_line_copy(buffer_t *buffer) { + if (!buffer) return NULL; + return buffer_substring(buffer, + buffer_current_line_start(buffer), + buffer_current_line_end(buffer)); +} + +int buffer_load_file(buffer_t *buffer, const char *path) { + if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR; + + size_t size = 0; + char *new_data = ecex_read_entire_file(path, &size); + if (!new_data) return ECEX_ERR; + + char *new_path = ecex_strdup(path); + if (!new_path) { + free(new_data); + return ECEX_ERR; + } + + free(buffer->data); + free(buffer->path); + + buffer->data = new_data; + buffer->path = new_path; + buffer->len = size; + buffer->cap = size + 1; + buffer->point = 0; + buffer->mark = 0; + buffer->mark_active = 0; + buffer->scroll_line = 0; + buffer->scroll_col = 0; + buffer->modified = 0; + buffer_clear_undo(buffer); + return ECEX_OK; +} + +int buffer_save(buffer_t *buffer) { + if (!buffer || buffer->read_only || !buffer->path) return ECEX_ERR; + + FILE *file = fopen(buffer->path, "wb"); + if (!file) return ECEX_ERR; + + size_t written = fwrite(buffer->data, 1, buffer->len, file); + int close_result = fclose(file); + + if (close_result != 0 || written != buffer->len) return ECEX_ERR; + + buffer->modified = 0; + return ECEX_OK; +} + +int buffer_save_as(buffer_t *buffer, const char *path) { + if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR; + + char *new_path = ecex_strdup(path); + if (!new_path) return ECEX_ERR; + + free(buffer->path); + buffer->path = new_path; + return buffer_save(buffer); +} + + + +int buffer_undo(buffer_t *buffer) { + if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; + if (buffer->undo_count == 0) return ECEX_OK; + if (buffer_push_snapshot_to(&buffer->redo_stack, &buffer->redo_count, &buffer->redo_cap, buffer) != ECEX_OK) return ECEX_ERR; + ecex_undo_entry_t entry = buffer->undo_stack[--buffer->undo_count]; + int result = buffer_restore_snapshot(buffer, &entry); + buffer_undo_entry_free(&entry); + return result; +} + +int buffer_redo(buffer_t *buffer) { + if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; + if (buffer->redo_count == 0) return ECEX_OK; + if (buffer_push_snapshot_to(&buffer->undo_stack, &buffer->undo_count, &buffer->undo_cap, buffer) != ECEX_OK) return ECEX_ERR; + ecex_undo_entry_t entry = buffer->redo_stack[--buffer->redo_count]; + int result = buffer_restore_snapshot(buffer, &entry); + buffer_undo_entry_free(&entry); + return result; +} + +void buffer_clear_undo(buffer_t *buffer) { + if (!buffer) return; + buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); + buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); + buffer->undo_count = 0; + buffer->redo_count = 0; +} + +int buffer_search_forward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) { + if (!buffer || !query || !query[0]) return ECEX_ERR; + if (start > buffer->len) start = buffer->len; + char *hit = strstr(buffer->data + start, query); + if (!hit && start > 0) hit = strstr(buffer->data, query); + if (!hit) return ECEX_ERR; + if (out_pos) *out_pos = (size_t)(hit - buffer->data); + return ECEX_OK; +} + +int buffer_search_backward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) { + if (!buffer || !query || !query[0]) return ECEX_ERR; + size_t qlen = strlen(query); + if (qlen > buffer->len) return ECEX_ERR; + if (start > buffer->len) start = buffer->len; + size_t best = (size_t)-1; + for (size_t i = 0; i + qlen <= start; i++) { + if (memcmp(buffer->data + i, query, qlen) == 0) best = i; + } + if (best == (size_t)-1) { + for (size_t i = start; i + qlen <= buffer->len; i++) { + if (memcmp(buffer->data + i, query, qlen) == 0) best = i; + } + } + if (best == (size_t)-1) return ECEX_ERR; + if (out_pos) *out_pos = best; + return ECEX_OK; +} + +void buffer_set_interactive(buffer_t *buffer, int interactive) { + if (!buffer) return; + buffer->interactive = interactive ? 1 : 0; +} + +int buffer_is_interactive(buffer_t *buffer) { + return buffer && buffer->interactive; +} + +int buffer_clear_interactive_actions(buffer_t *buffer) { + if (!buffer) return ECEX_ERR; + + for (size_t i = 0; i < buffer->interactive_action_count; i++) { + free(buffer->interactive_actions[i].payload); + buffer->interactive_actions[i].payload = NULL; + buffer->interactive_actions[i].fn = NULL; + buffer->interactive_actions[i].userdata = NULL; + buffer->interactive_actions[i].line = 0; + } + + buffer->interactive_action_count = 0; + return ECEX_OK; +} + +int buffer_add_interactive_action(buffer_t *buffer, + size_t line, + ecex_interactive_line_fn fn, + const char *payload, + void *userdata) { + if (!buffer || !fn) return ECEX_ERR; + + if (ECEX_GROW_ARRAY(buffer->interactive_actions, + buffer->interactive_action_count, + buffer->interactive_action_cap, + 16) != ECEX_OK) { + return ECEX_ERR; + } + + char *payload_copy = ecex_strdup(payload); + if (payload && !payload_copy) return ECEX_ERR; + + ecex_interactive_line_action_t *action = + &buffer->interactive_actions[buffer->interactive_action_count++]; + + action->line = line; + action->fn = fn; + action->payload = payload_copy; + action->userdata = userdata; + + buffer->interactive = 1; + return ECEX_OK; +} + +ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buffer, + size_t line) { + if (!buffer || !buffer->interactive) return NULL; + + for (size_t i = 0; i < buffer->interactive_action_count; i++) { + if (buffer->interactive_actions[i].line == line) { + return &buffer->interactive_actions[i]; + } + } + + return NULL; +} |
