From 4db6077738f862cbeee478453661ff344079fa39 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 28 Nov 2025 00:24:53 -0800 Subject: [PATCH] Splitting into separate files. --- CMakeLists.txt | 9 +- Makefile | 6 +- abuf.c | 77 ++++++ abuf.h | 27 ++ buffer.c | 456 +++++++++++++++++++++++++++++++ buffer.h | 31 +++ core.c | 60 +++++ core.h | 34 +++ main.c | 720 +++++++++++++++++++++++-------------------------- term.c | 86 ++++++ term.h | 22 ++ undo.c | 104 +++++++ undo.h | 44 +++ 13 files changed, 1287 insertions(+), 389 deletions(-) create mode 100644 abuf.c create mode 100644 abuf.h create mode 100644 buffer.c create mode 100644 buffer.h create mode 100644 core.c create mode 100644 core.h create mode 100644 term.c create mode 100644 term.h create mode 100644 undo.c create mode 100644 undo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index da9d149..ba54778 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,14 @@ endif() include(GNUInstallDirs) # Add executable -add_executable(ke main.c) +add_executable(ke + main.c + abuf.c + term.c + buffer.c + core.c + core.h +) target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}") install(TARGETS ke RUNTIME DESTINATION bin) install(FILES ke.1 TYPE MAN) diff --git a/Makefile b/Makefile index 7cd3713..65e33da 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,10 @@ LDFLAGS := -fsanitize=address all: $(TARGET) test.txt -$(TARGET): main.c - $(CC) $(CFLAGS) -o $(TARGET) $(LDFLAGS) main.c +SRCS := main.c abuf.c term.c buffer.c + +$(TARGET): $(SRCS) + $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS) .PHONY: install #install: $(TARGET) diff --git a/abuf.c b/abuf.c new file mode 100644 index 0000000..bcbd9d0 --- /dev/null +++ b/abuf.c @@ -0,0 +1,77 @@ +#include +#include +#include + +#include "abuf.h" + + +void +abuf_init(abuf *buf) +{ + assert(buf != NULL); + + buf->b = NULL; + buf->size = buf->cap = 0; +} + + +void +ab_appendch(abuf *buf, char c) +{ + ab_append(buf, &c, 1); +} + + +void +ab_append(abuf *buf, const char *s, size_t len) +{ + char *nc = buf->b; + size_t sz = buf->size + len; + + if (sz >= buf->cap) { + while (sz > buf->cap) { + if (buf->cap == 0) { + buf->cap = 1; + } else { + buf->cap *= 2; + } + } + nc = realloc(nc, buf->cap); + assert(nc != NULL); + } + + memcpy(&nc[buf->size], s, len); + buf->b = nc; + buf->size += len; +} + + +void +ab_prependch(abuf *buf, const char c) +{ + ab_prepend(buf, &c, 1); +} + + +void +ab_prepend(abuf *buf, const char *s, const size_t len) +{ + char *nc = realloc(buf->b, buf->size + len); + assert(nc != NULL); + + memmove(nc + len, nc, buf->size); + memcpy(nc, s, len); + + buf->b = nc; + buf->size += len; +} + + +void +ab_free(abuf *buf) +{ + free(buf->b); + buf->b = NULL; + buf->size = 0; + buf->cap = 0; +} diff --git a/abuf.h b/abuf.h new file mode 100644 index 0000000..ae77da2 --- /dev/null +++ b/abuf.h @@ -0,0 +1,27 @@ +/* + * abuf.h - append/prepend buffer utilities + */ +#ifndef ABUF_H +#define ABUF_H + +#include + + +typedef struct abuf { + char *b; + size_t size; + size_t cap; +} abuf; + +#define ABUF_INIT {NULL, 0, 0} + + +void ab_init(abuf *buf); +void ab_appendch(abuf *buf, char c); +void ab_append(abuf *buf, const char *s, size_t len); +void ab_prependch(abuf *buf, const char c); +void ab_prepend(abuf *buf, const char *s, const size_t len); +void ab_free(abuf *buf); + + +#endif diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..dcf2e53 --- /dev/null +++ b/buffer.c @@ -0,0 +1,456 @@ +/* buffer.c - buffer management implementation */ + +#include +#include +#include +#include +#include + +#include "abuf.h" +#include "buffer.h" +#include "editor.h" + + +#define NO_NAME "[No Name]" + +/* externs from other modules */ +char *editor_prompt(char *, void (*cb)(char *, int16_t)); + + +static const char * +buf_basename(const char *path) +{ + if (path == NULL) { + return NULL; + } + + const char *slash = strrchr(path, '/'); + return (slash != NULL) ? (slash + 1) : path; +} + +static int +buffer_find_exact_by_name(const char *name) +{ + buffer *b = NULL; + + if (name == NULL) { + return -1; + } + + for (int i = 0; i < editor.bufcount; i++) { + b = editor.buffers[i]; + const char *full = b->filename; + const char *base = buf_basename(full); + + if (full && strcmp(full, name) == 0) { + return i; + } + + if (base && strcmp(base, name) == 0) { + return i; + } + + if (full == NULL && strcmp(name, NO_NAME) == 0) { + return i; + } + } + + return -1; +} + + +static int +buffer_collect_prefix_matches(const char *prefix, int *out_idx, const int max_out) +{ + buffer *b = NULL; + int count = 0; + int matched = 0; + size_t plen = (prefix ? strlen(prefix) : 0); + + for (int i = 0; i < editor.bufcount; i++) { + b = editor.buffers[i]; + + const char *cand1 = b->filename; + const char *cand2 = buf_basename(cand1); + + if (plen == 0) { + matched = 1; /* everything matches empty prefix */ + } else { + if (cand2 && strncmp(cand2, prefix, plen) == 0) { + matched = 1; + } else if (cand1 && strncmp(cand1, prefix, plen) == 0) { + matched = 1; + } else if (!cand1 && strncmp(NO_NAME, prefix, plen) == 0) { + matched = 1; + } + } + + if (matched) { + if (count < max_out) { + out_idx[count] = i; + } + count++; + } + } + + return count; +} + + +static void +longest_common_prefix(char *buf, const size_t bufsz, const int *idxs, const int n) +{ + const char *first = NULL; + const char *cand = NULL; + int k = 0; + size_t j = 0; + size_t cur = 0; + size_t lcp = 0; + size_t to_copy = 0; + + if (n <= 0) { + return; + } + + first = buf_basename(editor.buffers[idxs[0]]->filename); + if (first == NULL) { + first = NO_NAME; + } + + lcp = strnlen(first, FILENAME_MAX); + for (k = 1; k < n; k++) { + cand = buf_basename(editor.buffers[idxs[k]]->filename); + if (cand == NULL) { + cand = NO_NAME; + } + + j = 0; + while (j < lcp && first[j] == cand[j]) { + j++; + } + + lcp = j; + if (lcp == 0) { + break; + } + } + + cur = strlen(buf); + if (lcp > cur) { + to_copy = lcp - cur; + if (cur + to_copy >= bufsz) { + to_copy = bufsz - cur - 1; + } + + strncat(buf, first + cur, to_copy); + } +} + + +static void +buffer_switch_prompt_cb(char *buf, const int16_t key) +{ + char msg[80] = {0}; + const char *name = NULL; + const char *nm = NULL; + int idxs[64] = {0}; + int n = 0; + size_t need = 0; + size_t used = 0; + + if (key != 9) { /* TODO(kyle): extract TAB_KEY */ + return; /* TAB key */ + } + + + n = buffer_collect_prefix_matches(buf, idxs, 64); + if (n <= 0) { + editor_set_status("No matches"); + return; + } + + if (n == 1) { + name = buf_basename(editor.buffers[idxs[0]]->filename); + if (name == NULL) { + name = NO_NAME; + } + + need = strlen(name); + if (need < 128) { + memcpy(buf, name, need); + buf[need] = '\0'; + } + + editor_set_status("Unique match: %s", name); + return; + } + + longest_common_prefix(buf, 128, idxs, n); + msg[0] = '\0'; + used = 0; + used += snprintf(msg + used, sizeof(msg) - used, "%d matches: ", n); + for (int i = 0; i < n && used < sizeof(msg) - 1; i++) { + nm = buf_basename(editor.buffers[idxs[i]]->filename); + if (nm == NULL) { + nm = NO_NAME; + } + + used += snprintf(msg + used, sizeof(msg) - used, "%s%s", + nm, (i == n - 1 ? "" : ", ")); + } + + editor_set_status("%s", msg); +} + + +static void +buffer_bind_to_editor(buffer *b) +{ + if (b == NULL) { + return; + } + + editor.curx = b->curx; + editor.cury = b->cury; + editor.rx = b->rx; + editor.nrows = b->nrows; + editor.rowoffs = b->rowoffs; + editor.coloffs = b->coloffs; + editor.row = b->row; + editor.filename = b->filename; + editor.dirty = b->dirty; + editor.mark_set = b->mark_set; + editor.mark_curx = b->mark_curx; + editor.mark_cury = b->mark_cury; +} + + +static void buffer_extract_from_editor(buffer *b) +{ + if (b == NULL) { + return; + } + + b->curx = editor.curx; + b->cury = editor.cury; + b->rx = editor.rx; + b->nrows = editor.nrows; + b->rowoffs = editor.rowoffs; + b->coloffs = editor.coloffs; + b->row = editor.row; + b->filename = editor.filename; + b->dirty = editor.dirty; + b->mark_set = editor.mark_set; + b->mark_curx = editor.mark_curx; + b->mark_cury = editor.mark_cury; +} + +const char * +buffer_name(buffer *b) +{ + if (b && b->filename) { + return buf_basename(b->filename); + } + + return NO_NAME; +} + + +void +buffers_init(void) +{ + int idx = 0; + + editor.buffers = NULL; + editor.bufcount = 0; + editor.curbuf = -1; + + idx = buffer_add_empty(); + buffer_switch(idx); +} + + +int +buffer_add_empty(void) +{ + buffer *buf = NULL; + buffer **newlist = realloc(editor.buffers, sizeof(buffer *) * (editor.bufcount + 1)); + int idx = 0; + + assert(newlist != NULL); + editor.buffers = newlist; + + buf = calloc(1, sizeof(buffer)); + assert(buf != NULL); + + buf->curx = 0; + buf->cury = 0; + buf->rx = 0; + buf->nrows = 0; + buf->rowoffs = 0; + buf->coloffs = 0; + buf->row = NULL; + buf->filename = NULL; + buf->dirty = 0; + buf->mark_set = 0; + buf->mark_curx = 0; + buf->mark_cury = 0; + + editor.buffers[editor.bufcount] = buf; + idx = editor.bufcount; + editor.bufcount++; + return idx; +} + + +void +buffer_save_current(void) +{ + buffer *b = NULL; + + if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) { + return; + } + + b = editor.buffers[editor.curbuf]; + buffer_extract_from_editor(b); +} + + +void +buffer_switch(const int idx) +{ + buffer *b = NULL; + + if (idx < 0 || idx >= editor.bufcount) { + return; + } + + if (editor.curbuf == idx) { + return; + } + + if (editor.curbuf >= 0) { + buffer_save_current(); + } + + b = editor.buffers[idx]; + buffer_bind_to_editor(b); + editor.curbuf = idx; + editor.dirtyex = 1; + editor_set_status("Switched to buffer %d: %s", editor.curbuf, buffer_name(b)); +} + + +void +buffer_next(void) +{ + int idx = 0; + + if (editor.bufcount <= 1) { + return; + } + + idx = (editor.curbuf + 1) % editor.bufcount; + buffer_switch(idx); +} + +void buffer_prev(void) +{ + int idx = 0; + + if (editor.bufcount <= 1) { + return; + } + + idx = (editor.curbuf - 1 + editor.bufcount) % editor.bufcount; + buffer_switch(idx); +} + + +void +buffer_close_current(void) +{ + buffer *b = NULL; + int closing = 0; + int target = 0; + int nb = 0; + + if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) { + editor_set_status("No buffer to close."); + return; + } + + closing = editor.curbuf; + + target = -1; + if (editor.bufcount > 1) { + target = (closing - 1 >= 0) ? (closing - 1) : (closing + 1); + buffer_switch(target); + } else { + nb = buffer_add_empty(); + buffer_switch(nb); + } + + b = editor.buffers[closing]; + if (b) { + if (b->row) { + for (int i = 0; i < b->nrows; i++) { + ab_free(&b->row[i]); + } + free(b->row); + } + + if (b->filename) { + free(b->filename); + } + free(b); + } + + memmove(&editor.buffers[closing], &editor.buffers[closing + 1], + sizeof(buffer *) * (editor.bufcount - closing - 1)); + + editor.bufcount--; + if (editor.bufcount == 0) { + editor.curbuf = -1; + } else { + if (editor.curbuf > closing) { + editor.curbuf--; + } + } + + editor.dirtyex = 1; + editor_set_status("Closed buffer. Now on %s", + buffer_name(editor.buffers[editor.curbuf])); +} + + +void +buffer_switch_by_name(void) +{ + int idxs[64] = {0}; + char *name = NULL; + int idx = 0; + int n = 0; + + name = editor_prompt("Switch buffer (name, TAB to complete): %s", buffer_switch_prompt_cb); + if (name == NULL) { + return; + } + + idx = buffer_find_exact_by_name(name); + if (idx < 0) { + n = buffer_collect_prefix_matches(name, idxs, 64); + if (n == 1) { + idx = idxs[0]; + } + } + + if (idx >= 0) { + buffer_switch(idx); + } else { + editor_set_status("No such buffer: %s", name); + } + + free(name); +} diff --git a/buffer.h b/buffer.h new file mode 100644 index 0000000..926ec8a --- /dev/null +++ b/buffer.h @@ -0,0 +1,31 @@ +#ifndef BUFFER_H +#define BUFFER_H + +#include "abuf.h" + + +typedef struct buffer { + int curx, cury; + int rx; + int nrows; + int rowoffs, coloffs; + abuf *row; + char *filename; + int dirty; + int mark_set; + int mark_curx, mark_cury; +} buffer; + + +void buffers_init(void); +int buffer_add_empty(void); +void buffer_save_current(void); +void buffer_switch(int idx); +void buffer_next(void); +void buffer_prev(void); +void buffer_switch_by_name(void); +void buffer_close_current(void); +const char *buffer_name(buffer *b); + + +#endif diff --git a/core.c b/core.c new file mode 100644 index 0000000..af7b351 --- /dev/null +++ b/core.c @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include + +#include "core.h" + +#ifdef INCLUDE_STRNSTR +/* + * Find the first occurrence of find in s, where the search is limited to the + * first slen characters of s. + */ +char * +strnstr(const char *s, const char *find, size_t slen) +{ + char c, sc; + size_t len; + + if ((c = *find++) != '\0') { + len = strlen(find); + do { + do { + if (slen-- < 1 || (sc = *s++) == '\0') + return (NULL); + } while (sc != c); + if (len > slen) + return (NULL); + } while (strncmp(s, find, len) != 0); + s--; + } + return ((char*)s); +} +#endif + + +void +kwrite(const int fd, const char* buf, const int len) +{ + int wlen = 0; + + wlen = write(fd, buf, len); + assert(wlen != -1); + assert(wlen == len); + if (wlen == -1) { + abort(); + } +} + + +void +die(const char* s) +{ + kwrite(STDOUT_FILENO, "\x1b[2J", 4); + kwrite(STDOUT_FILENO, "\x1b[H", 3); + + perror(s); + exit(1); +} diff --git a/core.h b/core.h new file mode 100644 index 0000000..74edae9 --- /dev/null +++ b/core.h @@ -0,0 +1,34 @@ +#ifndef KE_CORE_H +#define KE_CORE_H + + +#define calloc1(sz) calloc(1, sz) +#include + + +typedef enum key_press { + TAB_KEY = 9, + ESC_KEY = 27, + BACKSPACE = 127, + ARROW_LEFT = 1000, + ARROW_RIGHT = 1001, + ARROW_UP = 1002, + ARROW_DOWN = 1003, + DEL_KEY = 1004, + HOME_KEY = 1005, + END_KEY = 1006, + PG_UP = 1007, + PG_DN = 1008, +} key_press; + +#ifndef strnstr +char *strnstr(const char *s, const char *find, size_t slen); +#define INCLUDE_STRNSTR +#endif + + +void kwrite(int fd, const char *buf, int len); +void die(const char *s); + + +#endif \ No newline at end of file diff --git a/main.c b/main.c index 2a69dc7..c4bd615 100644 --- a/main.c +++ b/main.c @@ -36,6 +36,13 @@ #include #include #include +#include +#include + +#include "abuf.h" +#include "buffer.h" +#include "core.h" +#include "term.h" #ifndef KE_VERSION @@ -71,46 +78,8 @@ #define KILLRING_FLUSH 4 /* clear the killring */ -#define calloc1(sz) calloc(1, sz) - - -/* append buffer */ -typedef struct abuf { - char *b; - size_t size; - size_t cap; -} abuf; - -#define ABUF_INIT {NULL, 0, 0} - - -typedef enum undo_kind { - UNDO_INSERT = 1 << 0, - UNDO_UNKNOWN = 1 << 1, -} undo_kind; - - -typedef struct undo_node { - undo_kind op; - size_t row, col; - abuf text; - - struct undo_node *next; - struct undo_node *parent; - -} undo_node; - - -typedef struct undo_tree { - undo_node *root; /* the start of the undo sequence */ - undo_node *current; /* where we are currently at */ - undo_node *pending; /* the current undo operations being built */ -} undo_tree; - - /* - * editor is the global editor state; it should be broken out - * to buffers and screen state, probably. + * editor is the global editor state. */ struct editor { struct termios entry_term; @@ -132,6 +101,11 @@ struct editor { int mark_curx, mark_cury; int uarg, ucount; /* C-u support */ time_t msgtm; + + /* Multi-buffer support */ + struct buffer **buffers; /* buffer list */ + int bufcount; /* number of buffers */ + int curbuf; /* current buffer index */ } editor = { .cols = 0, .rows = 0, @@ -153,23 +127,23 @@ struct editor { .mark_cury = 0, .uarg = 0, .ucount = 0, + .buffers = NULL, + .bufcount = 0, + .curbuf = -1, }; void init_editor(void); void reset_editor(void); +/* buffers now declared in buffer.h */ + /* small tools, abufs, etc */ int next_power_of_2(int n); int cap_growth(int cap, int sz); size_t kstrnlen(const char *buf, size_t max); void ab_init(abuf *buf); void ab_init_cap(abuf *buf, size_t cap); -void ab_appendch(abuf *buf, char c); -void ab_append(abuf *buf, const char *s, size_t len); -void ab_prependch(abuf *buf, char c); -void ab_prepend(abuf *buf, const char *s, size_t len); -void ab_free(abuf *buf); char nibble_to_hex(char c); void swap_int(int *a, int *b); @@ -189,7 +163,7 @@ void delete_region(void); /* miscellaneous */ void kwrite(int fd, const char *buf, int len); void die(const char *s); -int get_winsz(int *rows, int *cols); +/* get_winsz now provided by term.h */ void jump_to_position(int col, int row); void goto_line(void); int cursor_at_eol(void); @@ -226,10 +200,7 @@ void process_escape(int16_t c); int process_keypress(void); char *get_cloc_code_lines(const char *filename); int dump_pidfile(void); -void enable_termraw(void); -void display_clear(abuf *ab); -void disable_termraw(void); -void setup_terminal(void); +/* terminal functions declared in term.h */ void draw_rows(abuf *ab); char status_mode_char(void); void draw_status_bar(abuf *ab); @@ -244,34 +215,6 @@ static void signal_handler(int sig); static void install_signal_handlers(void); -#ifndef strnstr -/* - * Find the first occurrence of find in s, where the search is limited to the - * first slen characters of s. - */ -char -*strnstr(const char *s, const char *find, size_t slen) -{ - char c, sc; - size_t len; - - if ((c = *find++) != '\0') { - len = strlen(find); - do { - do { - if (slen-- < 1 || (sc = *s++) == '\0') - return (NULL); - } while (sc != c); - if (len > slen) - return (NULL); - } while (strncmp(s, find, len) != 0); - s--; - } - return ((char*)s); -} -#endif - - int next_power_of_2(int n) { @@ -305,22 +248,6 @@ cap_growth(int cap, int sz) } -enum KeyPress { - TAB_KEY = 9, - ESC_KEY = 27, - BACKSPACE = 127, - ARROW_LEFT = 1000, - ARROW_RIGHT, - ARROW_UP, - ARROW_DOWN, - DEL_KEY, - HOME_KEY, - END_KEY, - PG_UP, - PG_DN, -}; - - size_t kstrnlen(const char *buf, const size_t max) { @@ -368,6 +295,11 @@ init_editor(void) editor.dirty = 0; editor.mark_set = 0; editor.mark_cury = editor.mark_curx = 0; + + /* initialize buffer system on first init */ + if (editor.buffers == NULL && editor.bufcount == 0) { + buffers_init(); + } } @@ -377,27 +309,32 @@ init_editor(void) void reset_editor(void) { + /* Clear current working set (does not reset terminal or buffers list) */ for (int i = 0; i < editor.nrows; i++) { ab_free(&editor.row[i]); } free(editor.row); - + editor.row = NULL; + editor.nrows = 0; + editor.rowoffs = editor.coloffs = 0; + editor.curx = editor.cury = 0; + editor.rx = 0; if (editor.filename != NULL) { free(editor.filename); editor.filename = NULL; } - - - init_editor(); + editor.dirty = 0; + editor.mark_set = 0; + editor.mark_cury = editor.mark_curx = 0; } void ab_init(abuf *buf) { - buf->b = NULL; - buf->size = 0; - buf->cap = 0; + buf->b = NULL; + buf->size = 0; + buf->cap = 0; } @@ -413,73 +350,169 @@ ab_init_cap(abuf *buf, const size_t cap) void ab_resize(abuf *buf, size_t cap) { - cap = cap_growth(buf->cap, cap); - buf->b = realloc(buf->b, cap); - assert(buf->b != NULL); - buf->cap = cap; + cap = cap_growth(buf->cap, cap); + buf->b = realloc(buf->b, cap); + assert(buf->b != NULL); + buf->cap = cap; } -void -ab_appendch(abuf *buf, char c) +/* Buffer management moved to buffer.c */ + +/* ========================= + * File open: TAB completion callback + * ========================= */ +static int path_is_dir(const char *path) { - ab_append(buf, &c, 1); + struct stat st; + if (path == NULL) return 0; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode); + } + return 0; } - -void -ab_append(abuf *buf, const char *s, size_t len) +static size_t str_lcp2(const char *a, const char *b) { - char *nc = buf->b; - size_t sz = buf->size + len; - - if (sz >= buf->cap) { - while (sz > buf->cap) { - if (buf->cap == 0) { - buf->cap = 1; - } else { - buf->cap *= 2; - } - } - nc = realloc(nc, buf->cap); - assert(nc != NULL); - } - - memcpy(&nc[buf->size], s, len); - buf->b = nc; - buf->size += len; + if (!a || !b) return 0; + size_t i = 0; + while (a[i] && b[i] && a[i] == b[i]) i++; + return i; } - -void -ab_prependch(abuf *buf, const char c) +static void file_open_prompt_cb(char *buf, int16_t key) { - ab_prepend(buf, &c, 1); + if (key != TAB_KEY) return; + + /* Determine directory and basename prefix */ + char dirpath[PATH_MAX]; + char base[256]; + const char *slash = strrchr(buf, '/'); + if (slash) { + size_t dlen = (size_t)(slash - buf); + if (dlen == 0) { + /* path like "/foo" -> dir is "/" */ + strcpy(dirpath, "/"); + } else { + if (dlen >= sizeof(dirpath)) dlen = sizeof(dirpath) - 1; + memcpy(dirpath, buf, dlen); + dirpath[dlen] = '\0'; + } + strncpy(base, slash + 1, sizeof(base) - 1); + base[sizeof(base) - 1] = '\0'; + } else { + strcpy(dirpath, "."); + strncpy(base, buf, sizeof(base) - 1); + base[sizeof(base) - 1] = '\0'; + } + + DIR *d = opendir(dirpath); + if (!d) { + editor_set_status("No such dir: %s", dirpath); + return; + } + + /* Collect matches */ + const char *names[128]; + int isdir[128]; + int n = 0; + size_t plen = strlen(base); + + struct dirent *de; + while ((de = readdir(d)) != NULL) { + const char *name = de->d_name; + /* Skip . and .. */ + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; + if (plen == 0 || strncmp(name, base, plen) == 0) { + if (n < 128) { + names[n] = strdup(name); + /* Build full path to test dir */ + char full[PATH_MAX]; + if (snprintf(full, sizeof(full), "%s/%s", dirpath, name) >= 0) { + isdir[n] = path_is_dir(full); + } else { + isdir[n] = 0; + } + n++; + } + } + } + closedir(d); + + if (n == 0) { + editor_set_status("No file matches '%s' in %s", base, dirpath); + return; + } + + /* Compute LCP across matches */ + size_t lcp = strlen(names[0]); + for (int i = 1; i < n; i++) { + size_t k = str_lcp2(names[0], names[i]); + if (k < lcp) lcp = k; + if (lcp == 0) break; + } + + /* Build new buffer string: dirpath + '/' (if not root and present) + completion */ + char newbuf[PATH_MAX]; + newbuf[0] = '\0'; + if (slash) { + /* Preserve original directory portion including trailing slash */ + size_t dlen = (size_t)(slash - buf); + if (dlen >= sizeof(newbuf)) dlen = sizeof(newbuf) - 1; + memcpy(newbuf, buf, dlen); + newbuf[dlen] = '\0'; + strncat(newbuf, "/", sizeof(newbuf) - strlen(newbuf) - 1); + } + + /* The part to append is: if unique -> full name (+ '/' if dir), else current base extended to LCP */ + if (n == 1) { + strncat(newbuf, names[0], sizeof(newbuf) - strlen(newbuf) - 1); + if (isdir[0]) { + strncat(newbuf, "/", sizeof(newbuf) - strlen(newbuf) - 1); + } + /* Replace buffer */ + strncpy(buf, newbuf[0] ? newbuf : names[0], 127); + buf[127] = '\0'; + editor_set_status("Unique match: %s%s", names[0], isdir[0] ? "/" : ""); + } else { + /* Extend to LCP */ + char ext[256]; + size_t cur = strlen(base); + if (lcp > cur) { + size_t to_copy = lcp - cur; + if (to_copy >= sizeof(ext)) to_copy = sizeof(ext) - 1; + memcpy(ext, names[0] + cur, to_copy); + ext[to_copy] = '\0'; + /* Always start from current base prefix */ + strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1); + strncat(newbuf, ext, sizeof(newbuf) - strlen(newbuf) - 1); + } else { + /* No extension possible, keep base as-is */ + strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1); + } + strncpy(buf, newbuf, 127); + buf[127] = '\0'; + + /* Show candidates */ + char msg[80]; + size_t used = 0; + used += snprintf(msg + used, sizeof(msg) - used, "%d matches: ", n); + for (int i = 0; i < n && used < sizeof(msg) - 1; i++) { + used += snprintf(msg + used, sizeof(msg) - used, "%s%s%s", + (i ? ", " : ""), names[i], isdir[i] ? "/" : ""); + } + editor_set_status("%s", msg); + } + + /* Free duplicated names */ + for (int i = 0; i < n; i++) free((void*)names[i]); } - -void -ab_prepend(abuf *buf, const char *s, const size_t len) -{ - char *nc = realloc(buf->b, buf->size + len); - assert(nc != NULL); - - memmove(nc + len, nc, buf->size); - memcpy(nc, s, len); - - buf->b = nc; - buf->size += len; -} +/* Close the current buffer. If dirty, require confirmation (press C-k c twice) */ +/* buffer_close_current now implemented in buffer.c */ -void -ab_free(abuf *buf) -{ - free(buf->b); - buf->b = NULL; - buf->size = 0; - buf->cap = 0; -} +/* abuf implementations moved to abuf.c */ char @@ -1001,55 +1034,6 @@ delete_region(void) } -void -kwrite(const int fd, const char* buf, const int len) -{ - int wlen = 0; - - wlen = write(fd, buf, len); - assert(wlen != -1); - assert(wlen == len); - if (wlen == -1) { - abort(); - } -} - - -void -die(const char* s) -{ - kwrite(STDOUT_FILENO, "\x1b[2J", 4); - kwrite(STDOUT_FILENO, "\x1b[H", 3); - - perror(s); - exit(1); -} - - -/* - * get_winsz uses the TIOCGWINSZ to get the window size. - * - * there's a fallback way to do this, too, that involves moving the - * cursor down and to the left \x1b[999C\x1b[999B. I'm going to skip - * on this for now because it's bloaty and this works on OpenBSD and - * Linux, at least. - */ -int -get_winsz(int *rows, int *cols) -{ - struct winsize ws; - - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || - ws.ws_col == 0) { - return -1; - } - - *cols = ws.ws_col; - *rows = ws.ws_row; - return 0; -} - - void jump_to_position(int col, int row) { @@ -1556,7 +1540,7 @@ save_exit: uint16_t is_arrow_key(int16_t c) { - switch (c) { + switch (c) { case ARROW_DOWN: case ARROW_LEFT: case ARROW_RIGHT: @@ -1669,48 +1653,57 @@ char buf[0] = '\0'; - while (1) { - editor_set_status(prompt, buf); - display_refresh(); + while (1) { + editor_set_status(prompt, buf); + display_refresh(); - while ((c = get_keypress()) <= 0); - if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) { - if (buflen != 0) { - buf[--buflen] = '\0'; - } - } else if (c == ESC_KEY || c == CTRL_KEY('g')) { - editor_set_status(""); - if (cb) { - cb(buf, c); - } + while ((c = get_keypress()) <= 0); + if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) { + if (buflen != 0) { + buf[--buflen] = '\0'; + } + } else if (c == ESC_KEY || c == CTRL_KEY('g')) { + editor_set_status(""); + if (cb) { + cb(buf, c); + } - free(buf); - return NULL; - } else if (c == '\r') { - if (buflen != 0) { - editor_set_status(""); - if (cb) { - cb(buf, c); - } + free(buf); + return NULL; + } else if (c == '\r') { + if (buflen != 0) { + editor_set_status(""); + if (cb) { + cb(buf, c); + } - return buf; - } - } else if ((c == TAB_KEY) || (c >= 0x20 && c < 0x7f)) { - if (buflen == bufsz - 1) { - bufsz *= 2; - buf = realloc(buf, bufsz); + return buf; + } + } else if (c == TAB_KEY) { + /* invoke completion callback without inserting a TAB */ + if (cb) { + cb(buf, c); + } + /* keep buflen in sync in case callback edited buf */ + buflen = strlen(buf); + } else if (c >= 0x20 && c < 0x7f) { + if (buflen == bufsz - 1) { + bufsz *= 2; + buf = realloc(buf, bufsz); - assert(buf != NULL); - } + assert(buf != NULL); + } - buf[buflen++] = (char)(c & 0xff); - buf[buflen] = '\0'; - } + buf[buflen++] = (char)(c & 0xff); + buf[buflen] = '\0'; + } - if (cb) { - cb(buf, c); - } - } + if (cb) { + cb(buf, c); + /* keep buflen in sync with any changes the callback made */ + buflen = strlen(buf); + } + } free(buf); return NULL; @@ -1841,16 +1834,20 @@ editor_find(void) void editor_openfile(void) { - char *filename; + char *filename; - /* TODO(kyle): combine with dirutils for tab-completion */ - filename = editor_prompt("Load file: %s", NULL); - if (filename == NULL) { - return; - } + /* Add TAB completion for path input */ + filename = editor_prompt("Load file: %s", file_open_prompt_cb); + if (filename == NULL) { + return; + } - open_file(filename); - free(filename); + /* Open into a new buffer */ + int nb = buffer_add_empty(); + buffer_switch(nb); + open_file(filename); + buffer_save_current(); + free(filename); } @@ -2156,9 +2153,8 @@ process_kcommand(int16_t c) editor_set_status("Jumped to mark"); break; case 'c': - len = editor.killring->size; - killring_flush(); - editor_set_status("Kill ring cleared (%d characters)", len); + /* Close current buffer (was kill ring clear; that moved to C-k f) */ + buffer_close_current(); break; case 'd': if (editor.curx == 0 && cursor_at_eol()) { @@ -2196,8 +2192,21 @@ process_kcommand(int16_t c) } editor_openfile(); break; - case 'f': - editor_find(); + case 'f': { + /* Rebound: clear kill ring on C-k f */ + len = editor.killring ? (int)editor.killring->size : 0; + killring_flush(); + editor_set_status("Kill ring cleared (%d characters)", len); + break; + } + case 'n': + buffer_next(); + break; + case 'p': + buffer_prev(); + break; + case 'b': + buffer_switch_by_name(); break; case 'g': goto_line(); @@ -2510,11 +2519,11 @@ process_keypress(void) char *get_cloc_code_lines(const char *filename) { - char command[512]; - char buffer[256]; - char *result = NULL; - FILE* pipe = NULL; - size_t len = 0; + char command[512]; + char outbuf[256]; + char *result = NULL; + FILE *pipe = NULL; + size_t len = 0; if (editor.filename == NULL) { snprintf(command, sizeof(command), @@ -2542,20 +2551,20 @@ char pipe = popen(command, "r"); if (!pipe) { snprintf(command, sizeof(command), "Error getting LOC: %s", strerror(errno)); - result = (char*)malloc(sizeof(buffer) + 1); + result = (char*)malloc(sizeof(outbuf) + 1); return NULL; } - if (fgets(buffer, sizeof(buffer), pipe) != NULL) { - len = strlen(buffer); - if (len > 0 && buffer[len - 1] == '\n') { - buffer[len - 1] = '\0'; + if (fgets(outbuf, sizeof(outbuf), pipe) != NULL) { + len = strlen(outbuf); + if (len > 0 && outbuf[len - 1] == '\n') { + outbuf[len - 1] = '\0'; } - result = malloc(strlen(buffer) + 1); + result = malloc(strlen(outbuf) + 1); assert(result != NULL); if (result) { - strcpy(result, buffer); + strcpy(result, outbuf); pclose(pipe); return result; } @@ -2588,80 +2597,7 @@ dump_pidfile(void) } -/* - * A text editor needs the terminal to be in raw mode; but the default - * is to be in canonical (cooked) mode, which is a buffered input mode. - */ -void -enable_termraw(void) -{ - struct termios raw; - - /* Read the current terminal parameters for standard input. */ - if (tcgetattr(STDIN_FILENO, &raw) == -1) { - die("tcgetattr while enabling raw mode"); - } - - /* - * Put the terminal into raw mode. - */ - cfmakeraw(&raw); - - /* - * Set timeout for read(2). - * - * VMIN: what is the minimum number of bytes required for read - * to return? - * - * VTIME: max time before read(2) returns in hundreds of milli- - * seconds. - */ - raw.c_cc[VMIN] = 0; - raw.c_cc[VTIME] = 1; - - /* - * Now write the terminal parameters to the current terminal, - * after flushing any waiting input out. - */ - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { - die("tcsetattr while enabling raw mode"); - } -} - - -void -display_clear(abuf *ab) -{ - if (ab == NULL) { - kwrite(STDOUT_FILENO, ESCSEQ "2J", 4); - kwrite(STDOUT_FILENO, ESCSEQ "H", 3); - } else { - ab_append(ab, ESCSEQ "2J", 4); - ab_append(ab, ESCSEQ "H", 3); - } -} - - -void -disable_termraw(void) -{ - display_clear(NULL); - - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.entry_term) == -1) { - die("couldn't disable terminal raw mode"); - } -} - - -void -setup_terminal(void) -{ - if (tcgetattr(STDIN_FILENO, &editor.entry_term) == -1) { - die("can't snapshot terminal settings"); - } - - enable_termraw(); -} +/* terminal control moved to term.c */ void @@ -3012,66 +2948,78 @@ install_signal_handlers(void) int main(int argc, char *argv[]) { - char *fname = NULL; - char *lnarg = NULL; - int lineno = 0; - int opt; - int debug = 0; - int jump = 0; + int opt; + int debug = 0; + /* argv processing for multiple files and +lineno */ + int pending_line = 0; /* line number that applies to next filename */ + int first_loaded = 0; /* whether we've opened the first file into initial buffer */ - install_signal_handlers(); + install_signal_handlers(); - while ((opt = getopt(argc, argv, "df:")) != -1) { - switch (opt) { - case 'd': - debug = 1; - break; - default: - fprintf(stderr, "Usage: ke [-d] [-f logfile] [path]\n"); - exit(EXIT_FAILURE); - } - } + while ((opt = getopt(argc, argv, "df:")) != -1) { + switch (opt) { + case 'd': + debug = 1; + break; + default: + fprintf(stderr, "Usage: ke [-d] [-f logfile] [ +N ] [file ...]\n"); + exit(EXIT_FAILURE); + } + } - argc -= optind; - argv += optind; + argc -= optind; + argv += optind; setlocale(LC_ALL, ""); if (debug) { enable_debugging(); } - setup_terminal(); - init_editor(); + setup_terminal(); + init_editor(); - if (argc > 0) { - fname = argv[0]; - } + /* Process remaining argv: accept multiple filenames; a "+N" applies to next filename */ + for (int i = 0; i < argc; i++) { + const char *arg = argv[i]; + if (arg[0] == '+') { + /* parse line number; if invalid, set to 0 (ignored) */ + const char *p = arg + 1; + int v = 0; + if (*p != '\0') { + v = atoi(p); + if (v < 1) v = 0; + } + pending_line = v; + continue; + } - if (argc > 1) { - lnarg = argv[0]; - fname = argv[1]; - if (lnarg[0] == '+') { - lnarg++; - } - lineno = atoi(lnarg); - jump = 1; - } + /* It's a filename */ + if (!first_loaded) { + /* initial empty buffer already exists; load into it */ + open_file(arg); + if (pending_line > 0) { + jump_to_position(0, pending_line - 1); + pending_line = 0; + } + buffer_save_current(); + first_loaded = 1; + } else { + /* create and switch to a new buffer for subsequent files */ + int nb = buffer_add_empty(); + buffer_switch(nb); + open_file(arg); + if (pending_line > 0) { + jump_to_position(0, pending_line - 1); + pending_line = 0; + } + buffer_save_current(); + } + } - if (fname != NULL) { - open_file(fname); - } + editor_set_status("C-k q to exit / C-k d to dump core"); - editor_set_status("C-k q to exit / C-k d to dump core"); - if (jump) { - if (lineno < 1) { - editor_set_status("Invalid line number %s", lnarg); - } else { - jump_to_position(0, lineno - 1); - } - } + display_clear(NULL); + loop(); - display_clear(NULL); - loop(); - - return 0; + return 0; } diff --git a/term.c b/term.c new file mode 100644 index 0000000..0995dad --- /dev/null +++ b/term.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "abuf.h" +#include "core.h" +#include "term.h" + +#define ESCSEQ "\x1b[" + + +static struct termios saved_entry_term; + + +void +enable_termraw(void) +{ + struct termios raw; + + if (tcgetattr(STDIN_FILENO, &raw) == -1) { + die("tcgetattr while enabling raw mode"); + } + + cfmakeraw(&raw); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { + die("tcsetattr while enabling raw mode"); + } +} + + +void +display_clear(abuf *ab) +{ + if (ab == NULL) { + kwrite(STDOUT_FILENO, ESCSEQ "2J", 4); + kwrite(STDOUT_FILENO, ESCSEQ "H", 3); + } else { + ab_append(ab, ESCSEQ "2J", 4); + ab_append(ab, ESCSEQ "H", 3); + } +} + + +void +disable_termraw(void) +{ + display_clear(NULL); + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_entry_term) == -1) { + die("couldn't disable terminal raw mode"); + } +} + + +void +setup_terminal(void) +{ + if (tcgetattr(STDIN_FILENO, &saved_entry_term) == -1) { + die("can't snapshot terminal settings"); + } + + enable_termraw(); +} + + +int +get_winsz(int *rows, int *cols) +{ + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + return -1; + } + + *cols = ws.ws_col; + *rows = ws.ws_row; + + return 0; +} diff --git a/term.h b/term.h new file mode 100644 index 0000000..6575672 --- /dev/null +++ b/term.h @@ -0,0 +1,22 @@ +#ifndef TERM_H +#define TERM_H + +#include "abuf.h" + +/* Terminal control/setup API */ +void enable_termraw(void); +void disable_termraw(void); +void setup_terminal(void); +void display_clear(abuf *ab); + +/* + * get_winsz uses the TIOCGWINSZ to get the window size. + * + * there's a fallback way to do this, too, that involves moving the + * cursor down and to the left \x1b[999C\x1b[999B. I'm going to skip + * on this for now because it's bloaty and this works on OpenBSD and + * Linux, at least. + */ +int get_winsz(int *rows, int *cols); + +#endif /* TERM_H */ diff --git a/undo.c b/undo.c new file mode 100644 index 0000000..a900507 --- /dev/null +++ b/undo.c @@ -0,0 +1,104 @@ +#include "abuf.h" +#include "undo.h" + + +undo_node +undo_node_new(undo_kind kind) +{ + undo_node *node = NULL; + + node = (undo_node *)malloc(sizeof(undo_node)); + assert(node != NULL); + + node->kind = kind; + node->row = node->col = 0; + + abuf_init(node->text); + + node->next = NULL; + node->parent = NULL; +} + + +void +undo_node_free(undo_node *node) +{ + undo_node *next = NULL; + + if (node == NULL) { + return NULL; + } + + abuf_free(node-text); + next = node->next; +} + + +void +undo_node_free_all(undo_node *node) +{ + undo_node *next = NULL; + + if (node == NULL) { + return; + } + + while (node != NULL) { + undo_node_free(node); + free(node); + node = node->next; + } +} + + +void +undo_tree_init(undo_tree *tree) +{ + assert(tree != NULL); + + tree->root = NULL; + tree->current = NULL; + tree->pending = NULL; +} + + +void +undo_tree_free(undo_tree *tree) +{ + assert(tree == NULL); + + undo_node_free(tree->pending); + undo_node_free_all(tree->root); + undo_tree_init(tree); +} + + +void +undo_begin(undo_tree *tree, undo_kind kind) +{ + undo_node *pending = NULL; + + if (tree->pending != NULL) { + if (tree->pending->kind == kind) { + /* don't initiate a new undo sequence if it's the same kind */ + return; + } + undo_commit(tree); + } + + pending = undo_new_new(kind); + assert(pending != NULL); + + tree->pending = pending; +} + + +void undo_prepend(abuf *buf); +void undo_append(buf *buf); +void undo_prependch(char c); +void undo_appendch(char c); +void undo_commit(void); +void undo_apply(undo_node *node); +void editor_undo(void); +void editor_redo(void); + diff --git a/undo.h b/undo.h new file mode 100644 index 0000000..6c4bf1b --- /dev/null +++ b/undo.h @@ -0,0 +1,44 @@ +#ifndef KE_UNDO +#define KE_UNDO + + +typedef enum undo_kind { + UNDO_INSERT = 1 << 0, + UNDO_UNKNOWN = 1 << 1, +} undo_kind; + + +typedef struct undo_node { + undo_kind op; + size_t row, col; + abuf text; + + struct undo_node *next; + struct undo_node *parent; + +} undo_node; + + +typedef struct undo_tree { + undo_node *root; /* the start of the undo sequence */ + undo_node *current; /* where we are currently at */ + undo_node *pending; /* the current undo operations being built */ +} undo_tree; + + +undo_node *undo_node_new(undo_kind kind); +void undo_node_free(undo_node *node); +void undo_tree_init(undo_tree *tree); +void undo_tree_free(undo_tree *tree); +void undo_begin(undo_tree *tree, undo_kind kind); +void undo_prepend(abuf *buf); +void undo_append(buf *buf); +void undo_prependch(char c); +void undo_appendch(char c); +void undo_commit(undo_tree *tree); +void undo_apply(undo_node *node); +void editor_undo(void); +void editor_redo(void); + + +#endif