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