5 Commits

Author SHA1 Message Date
0273a5ebb3 add undo docs 2026-04-02 16:03:45 -07:00
0cfb06dff2 junie-undo 2025-11-29 11:55:55 -08:00
a574df2ab7 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-28 15:20:11 -08:00
a9bcb0d36b Major codebase cleanup and overhaul.
+ editor removes per-buffer fields.
+ switching from internal use of 'int' to 'size_t'.
+ deleting old code
+ double checking relevancy of comments. A lot has changed in
  5 years, even more so in the past week.
+ fixing a few vestigal memory errors from the overhaul.
+ fixing search behavior
2025-11-28 15:19:47 -08:00
7b20e9ee37 Fix segfault. 2025-11-28 10:59:55 -08:00
17 changed files with 1344 additions and 968 deletions

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15)
project(ke C) # Specify C language explicitly project(ke C) # Specify C language explicitly
set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD 99)
set(KE_VERSION "2.0.1") set(KE_VERSION "2.1.0")
set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g -Werror=stringop-truncation") set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g -Werror=stringop-truncation")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE")
@@ -28,6 +28,8 @@ add_executable(ke
core.c core.c
core.h core.h
main.c main.c
undo.h
undo.c
) )
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}") target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
install(TARGETS ke RUNTIME DESTINATION bin) install(TARGETS ke RUNTIME DESTINATION bin)

52
abuf.c
View File

@@ -1,6 +1,7 @@
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdint-gcc.h>
#include "abuf.h" #include "abuf.h"
#include "core.h" #include "core.h"
@@ -28,19 +29,34 @@ ab_init(abuf *buf)
void void
ab_init_cap(abuf *buf, const size_t cap) ab_init_cap(abuf *buf, const size_t cap)
{ {
buf->b = calloc(cap, 1); ab_init(buf);
buf->size = 0;
buf->cap = cap; if (cap > 0) {
ab_resize(buf, cap);
}
}
void
ab_init_str(abuf *buf, const char *s)
{
size_t len = kstrnlen(s, SIZE_MAX);
ab_init_cap(buf, len);
ab_append(buf, s, len);
} }
void void
ab_resize(abuf *buf, size_t cap) ab_resize(abuf *buf, size_t cap)
{ {
cap = cap_growth(buf->cap, cap); char *newbuf = NULL;
buf->b = realloc(buf->b, cap);
assert(buf->b != NULL); cap = cap_growth(buf->cap, cap) + 1;
newbuf = realloc(buf->b, cap);
assert(newbuf != NULL);
buf->cap = cap; buf->cap = cap;
buf->b = newbuf;
} }
@@ -66,6 +82,18 @@ ab_append(abuf *buf, const char *s, size_t len)
} }
void
ab_append_ab(abuf *buf, abuf *other)
{
assert(buf != NULL && other != NULL);
if (other->size == 0) {
return;
}
ab_append(buf, other->b, other->size);
}
void void
ab_prependch(abuf *buf, const char c) ab_prependch(abuf *buf, const char c)
{ {
@@ -92,6 +120,18 @@ ab_prepend(abuf *buf, const char *s, const size_t len)
} }
void
ab_prepend_ab(abuf *buf, abuf *other)
{
assert(buf != NULL && other != NULL);
if (other->size == 0) {
return;
}
ab_prepend(buf, other->b, other->size);
}
void void
ab_free(abuf *buf) ab_free(abuf *buf)
{ {

11
abuf.h
View File

@@ -1,8 +1,8 @@
/* /*
* abuf.h - append/prepend buffer utilities * abuf.h - append/prepend buffer utilities
*/ */
#ifndef ABUF_H #ifndef KE_ABUF_H
#define ABUF_H #define KE_ABUF_H
#include <stddef.h> #include <stddef.h>
@@ -19,11 +19,14 @@ typedef struct abuf {
void ab_init(abuf *buf); void ab_init(abuf *buf);
void ab_init_cap(abuf *buf, size_t cap); void ab_init_cap(abuf *buf, size_t cap);
void ab_init_str(abuf *buf, const char *s);
void ab_resize(abuf *buf, size_t cap); void ab_resize(abuf *buf, size_t cap);
void ab_appendch(abuf *buf, char c); void ab_appendch(abuf *buf, char c);
void ab_append(abuf *buf, const char *s, size_t len); void ab_append(abuf *buf, const char *s, size_t len);
void ab_prependch(abuf *buf, const char c); void ab_append_ab(abuf *buf, abuf *other);
void ab_prepend(abuf *buf, const char *s, const size_t len); void ab_prependch(abuf *buf, char c);
void ab_prepend(abuf *buf, const char *s, size_t len);
void ab_prepend_ab(abuf *buf, abuf *other);
void ab_free(abuf *buf); void ab_free(abuf *buf);

107
buffer.c
View File

@@ -35,12 +35,13 @@ static int
buffer_find_exact_by_name(const char *name) buffer_find_exact_by_name(const char *name)
{ {
buffer *b = NULL; buffer *b = NULL;
size_t i = 0;
if (name == NULL) { if (name == NULL) {
return -1; return -1;
} }
for (int i = 0; i < editor.bufcount; i++) { for (i = 0; i < editor.bufcount; i++) {
b = editor.buffers[i]; b = editor.buffers[i];
const char *full = b->filename; const char *full = b->filename;
const char *base = buf_basename(full); const char *base = buf_basename(full);
@@ -68,9 +69,10 @@ buffer_collect_prefix_matches(const char *prefix, int *out_idx, const int max_ou
buffer *b = NULL; buffer *b = NULL;
int count = 0; int count = 0;
int matched = 0; int matched = 0;
size_t i = 0;
size_t plen = (prefix ? strlen(prefix) : 0); size_t plen = (prefix ? strlen(prefix) : 0);
for (int i = 0; i < editor.bufcount; i++) { for (i = 0; i < editor.bufcount; i++) {
matched = 0; matched = 0;
b = editor.buffers[i]; b = editor.buffers[i];
@@ -206,49 +208,6 @@ buffer_switch_prompt_cb(char *buf, const int16_t key)
} }
static void
buffer_bind_to_editor(const 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 * const char *
buffer_name(buffer *b) buffer_name(buffer *b)
{ {
@@ -267,7 +226,7 @@ buffers_init(void)
editor.buffers = NULL; editor.buffers = NULL;
editor.bufcount = 0; editor.bufcount = 0;
editor.curbuf = -1; editor.curbuf = 0;
editor.bufcap = 0; editor.bufcap = 0;
idx = buffer_add_empty(); idx = buffer_add_empty();
@@ -280,8 +239,8 @@ buffer_list_resize(void)
{ {
buffer **newlist = NULL; buffer **newlist = NULL;
if (editor.bufcount == (int)editor.bufcap) { if (editor.bufcount == editor.bufcap) {
editor.bufcap = cap_growth((int)editor.bufcap, editor.bufcount + 1); editor.bufcap = (size_t)cap_growth((int)editor.bufcap, (int)editor.bufcount + 1);
newlist = realloc(editor.buffers, sizeof(buffer *) * editor.bufcap); newlist = realloc(editor.buffers, sizeof(buffer *) * editor.bufcap);
assert(newlist != NULL); assert(newlist != NULL);
@@ -313,9 +272,11 @@ buffer_add_empty(void)
buf->mark_set = 0; buf->mark_set = 0;
buf->mark_curx = 0; buf->mark_curx = 0;
buf->mark_cury = 0; buf->mark_cury = 0;
/* initialize undo tree for this buffer */
undo_tree_init(&buf->undo);
editor.buffers[editor.bufcount] = buf; editor.buffers[editor.bufcount] = buf;
idx = editor.bufcount; idx = (int)editor.bufcount;
editor.bufcount++; editor.bufcount++;
return idx; return idx;
} }
@@ -324,21 +285,15 @@ buffer_add_empty(void)
void void
buffer_save_current(void) buffer_save_current(void)
{ {
buffer *b = NULL; /* No-op: editor no longer mirrors per-buffer fields */
(void)editor;
if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) {
return;
}
b = editor.buffers[editor.curbuf];
buffer_extract_from_editor(b);
} }
buffer * buffer *
buffer_current(void) buffer_current(void)
{ {
if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) { if (editor.bufcount == 0 || editor.curbuf >= editor.bufcount) {
return NULL; return NULL;
} }
return editor.buffers[editor.curbuf]; return editor.buffers[editor.curbuf];
@@ -377,21 +332,16 @@ buffer_switch(const int idx)
{ {
buffer *b = NULL; buffer *b = NULL;
if (idx < 0 || idx >= editor.bufcount) { if (idx < 0 || (size_t)idx >= editor.bufcount) {
return; return;
} }
if (editor.curbuf == idx) { if (editor.curbuf == (size_t)idx) {
return; return;
} }
if (editor.curbuf >= 0) {
buffer_save_current();
}
b = editor.buffers[idx]; b = editor.buffers[idx];
buffer_bind_to_editor(b); editor.curbuf = (size_t)idx;
editor.curbuf = idx;
editor.dirtyex = 1; editor.dirtyex = 1;
editor_set_status("Switched to buffer %d: %s", editor.curbuf, buffer_name(b)); editor_set_status("Switched to buffer %d: %s", editor.curbuf, buffer_name(b));
} }
@@ -400,28 +350,28 @@ buffer_switch(const int idx)
void void
buffer_next(void) buffer_next(void)
{ {
int idx = 0; size_t idx = 0;
if (editor.bufcount <= 1) { if (editor.bufcount <= 1) {
return; return;
} }
idx = (editor.curbuf + 1) % editor.bufcount; idx = (editor.curbuf + 1) % editor.bufcount;
buffer_switch(idx); buffer_switch((int)idx);
} }
void void
buffer_prev(void) buffer_prev(void)
{ {
int idx = 0; size_t idx = 0;
if (editor.bufcount <= 1) { if (editor.bufcount <= 1) {
return; return;
} }
idx = (editor.curbuf - 1 + editor.bufcount) % editor.bufcount; idx = (editor.curbuf == 0) ? (editor.bufcount - 1) : (editor.curbuf - 1);
buffer_switch(idx); buffer_switch((int)idx);
} }
@@ -429,21 +379,20 @@ void
buffer_close_current(void) buffer_close_current(void)
{ {
buffer *b = NULL; buffer *b = NULL;
int closing = 0; size_t closing = 0;
int target = 0; int target = 0;
int nb = 0; int nb = 0;
/* sanity check */ /* sanity check */
if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) { if (editor.bufcount == 0 || editor.curbuf >= editor.bufcount) {
editor_set_status("No buffer to close."); editor_set_status("No buffer to close.");
return; return;
} }
closing = editor.curbuf; closing = editor.curbuf;
target = -1;
if (editor.bufcount > 1) { if (editor.bufcount > 1) {
target = (closing - 1 >= 0) ? (closing - 1) : (closing + 1); target = (closing > 0) ? (int) (closing - 1) : (int) (closing + 1);
buffer_switch(target); buffer_switch(target);
} else { } else {
nb = buffer_add_empty(); nb = buffer_add_empty();
@@ -452,8 +401,10 @@ buffer_close_current(void)
b = editor.buffers[closing]; b = editor.buffers[closing];
if (b) { if (b) {
/* free undo tree resources before freeing buffer */
undo_tree_free(&b->undo);
if (b->row) { if (b->row) {
for (int i = 0; i < b->nrows; i++) { for (size_t i = 0; i < b->nrows; i++) {
ab_free(&b->row[i]); ab_free(&b->row[i]);
} }
free(b->row); free(b->row);
@@ -470,7 +421,7 @@ buffer_close_current(void)
editor.bufcount--; editor.bufcount--;
if (editor.bufcount == 0) { if (editor.bufcount == 0) {
editor.curbuf = -1; editor.curbuf = 0;
} else { } else {
if (editor.curbuf > closing) { if (editor.curbuf > closing) {
editor.curbuf--; editor.curbuf--;
@@ -512,7 +463,7 @@ buffer_switch_by_name(void)
if (idx >= 0) { if (idx >= 0) {
buffer_switch(idx); buffer_switch(idx);
} else { } else {
editor_set_status("No such buffer: %s", name); editor_set_status("No open buffer: %s", name);
} }
free(name); free(name);

View File

@@ -1,19 +1,21 @@
#ifndef BUFFER_H #ifndef KE_BUFFER_H
#define BUFFER_H #define KE_BUFFER_H
#include "abuf.h" #include "abuf.h"
#include "undo.h"
typedef struct buffer { typedef struct buffer {
int curx, cury; size_t curx, cury;
int rx; size_t rx;
int nrows; size_t nrows;
int rowoffs, coloffs; size_t rowoffs, coloffs;
abuf *row; abuf *row;
char *filename; char *filename;
int dirty; int dirty;
int mark_set; int mark_set;
int mark_curx, mark_cury; size_t mark_curx, mark_cury;
undo_tree undo;
} buffer; } buffer;
/* Access current buffer and convenient aliases for file-specific fields */ /* Access current buffer and convenient aliases for file-specific fields */

13
core.c
View File

@@ -35,19 +35,8 @@ strnstr(const char *s, const char *find, size_t slen)
#endif #endif
char
nibble_to_hex(char c)
{
c &= 0xf;
if (c < 10) {
return (char)('0' + c);
}
return (char)('A' + (c - 10));
}
void void
swap_int(int *first, int *second) swap_size_t(size_t *first, size_t *second)
{ {
*first ^= *second; *first ^= *second;
*second ^= *first; *second ^= *first;

4
core.h
View File

@@ -4,7 +4,6 @@
#include <stddef.h> #include <stddef.h>
#define calloc1(sz) calloc(1, sz)
#define INITIAL_CAPACITY 8 #define INITIAL_CAPACITY 8
@@ -29,8 +28,7 @@ char *strnstr(const char *s, const char *find, size_t slen);
#define INCLUDE_STRNSTR #define INCLUDE_STRNSTR
#endif #endif
char nibble_to_hex(char c); void swap_size_t(size_t *first, size_t *second);
void swap_int(int *first, int *second);
int next_power_of_2(int n); int next_power_of_2(int n);
int cap_growth(int cap, int sz); int cap_growth(int cap, int sz);
size_t kstrnlen(const char *buf, size_t max); size_t kstrnlen(const char *buf, size_t max);

View File

@@ -18,29 +18,18 @@
struct editor editor = { struct editor editor = {
.cols = 0, .cols = 0,
.rows = 0, .rows = 0,
.curx = 0,
.cury = 0,
.rx = 0,
.mode = 0, .mode = 0,
.nrows = 0,
.rowoffs = 0,
.coloffs = 0,
.row = NULL,
.killring = NULL, .killring = NULL,
.kill = 0, .kill = 0,
.no_kill = 0, .no_kill = 0,
.filename = NULL,
.dirty = 0,
.dirtyex = 0, .dirtyex = 0,
.mark_set = 0,
.mark_curx = 0,
.mark_cury = 0,
.uarg = 0, .uarg = 0,
.ucount = 0, .ucount = 0,
.msgtm = 0, .msgtm = 0,
.buffers = NULL, .buffers = NULL,
.bufcount = 0, .bufcount = 0,
.curbuf = -1, .curbuf = 0,
.bufcap = 0,
}; };
@@ -69,13 +58,6 @@ init_editor(void)
editor.rows--; /* status bar */ editor.rows--; /* status bar */
editor.rows--; /* message line */ editor.rows--; /* message line */
editor.curx = editor.cury = 0;
editor.rx = 0;
editor.nrows = 0;
editor.rowoffs = editor.coloffs = 0;
editor.row = NULL;
/* don't clear out the kill ring: /* don't clear out the kill ring:
* killing / yanking across files is helpful, and killring * killing / yanking across files is helpful, and killring
* is initialized to NULL at program start. * is initialized to NULL at program start.
@@ -86,10 +68,6 @@ init_editor(void)
editor.msg[0] = '\0'; editor.msg[0] = '\0';
editor.msgtm = 0; editor.msgtm = 0;
editor.dirty = 0;
editor.mark_set = 0;
editor.mark_cury = editor.mark_curx = 0;
/* initialize buffer system on first init */ /* initialize buffer system on first init */
if (editor.buffers == NULL && editor.bufcount == 0) { if (editor.buffers == NULL && editor.bufcount == 0) {
editor.bufcap = 0; editor.bufcap = 0;
@@ -101,25 +79,31 @@ init_editor(void)
void void
reset_editor(void) reset_editor(void)
{ {
/* Clear current working set. Notably, does not reset terminal /* Reset the current buffer's contents/state. */
* or buffers list. */ buffer *b = buffer_current();
for (int i = 0; i < editor.nrows; i++) { if (b == NULL) {
ab_free(&editor.row[i]); return;
}
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;
} }
editor.dirty = 0; if (b->row) {
editor.mark_set = 0; for (size_t i = 0; i < b->nrows; i++) {
editor.mark_cury = editor.mark_curx = 0; ab_free(&b->row[i]);
}
free(b->row);
}
b->row = NULL;
b->nrows = 0;
b->rowoffs = 0;
b->coloffs = 0;
b->rx = 0;
b->curx = 0;
b->cury = 0;
if (b->filename) {
free(b->filename);
b->filename = NULL;
}
b->dirty = 0;
b->mark_set = 0;
b->mark_curx = 0;
b->mark_cury = 0;
} }

View File

@@ -1,5 +1,5 @@
#ifndef EDITOR_H #ifndef KE_EDITOR_H
#define EDITOR_H #define KE_EDITOR_H
#include <termios.h> #include <termios.h>
#include <time.h> #include <time.h>
@@ -9,32 +9,19 @@
#include "buffer.h" #include "buffer.h"
/* TODO(kyle): remove the "per-buffer" fields completely from the editor. */
struct editor { struct editor {
int rows, cols; size_t rows, cols;
int curx, cury; /* per-buffer */
int rx; /* per-buffer */
int mode; int mode;
int nrows; /* per-buffer */
int rowoffs, coloffs; /* per-buffer */
abuf *row; /* per-buffer */
abuf *killring; abuf *killring;
int kill; /* KILL CHAIN (\m/) */ int kill; /* KILL CHAIN (\m/) */
int no_kill; /* don't kill in delete_row */ int no_kill; /* don't kill in delete_row */
char *filename; /* per-buffer */
int dirty; /* per-buffer */
int dirtyex; int dirtyex;
char msg[80]; char msg[80];
int mark_set; /* per-buffer */
int mark_curx, mark_cury; /* per-buffer */
int uarg, ucount; /* C-u support */ int uarg, ucount; /* C-u support */
time_t msgtm; time_t msgtm;
/* Multi-buffer support */
struct buffer **buffers; /* array of buffers */ struct buffer **buffers; /* array of buffers */
int bufcount; /* number of buffers */ size_t bufcount; /* number of buffers */
int curbuf; /* current buffer index */ size_t curbuf; /* current buffer index */
size_t bufcap; /* current buffer capacity */ size_t bufcap; /* current buffer capacity */
}; };
@@ -45,4 +32,4 @@ void init_editor(void);
void reset_editor(void); void reset_editor(void);
#endif /* EDITOR_H */ #endif /* KE_EDITOR_H */

4
ke.1
View File

@@ -65,9 +65,9 @@ Reload the current buffer from disk.
.It C-k s .It C-k s
Save the file, prompting for a filename if needed. Also C-k C-s. Save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k u .It C-k u
Undo changes (not implemented; marking this k-command as taken). Undo changes.
.It C-k U .It C-k U
Redo changes (not implemented; marking this k-command as taken). Redo changes.
.It C-k x .It C-k x
save the file and exit. Also C-k C-x. save the file and exit. Also C-k C-x.
.It C-k y .It C-k y

701
main.c

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,18 @@
* scratch.c - ideas in progress * scratch.c - ideas in progress
*/ */
#include "buffer.h"
#include "abuf.h"
#include <ctype.h>
#include <string.h>
#define REFLOW_MARGIN 72 #define REFLOW_MARGIN 72
void void
reflow_region(void) reflow_region(void)
{ {
int start_row, end_row, i, col, wlen, this_len; int start_row, end_row, i, col, wlen, this_len;
struct erow *row; abuf *row;
struct abuf buf = ABUF_INIT; struct abuf buf = ABUF_INIT;
struct abuf out = ABUF_INIT; struct abuf out = ABUF_INIT;
int in_paragraph = 0; int in_paragraph = 0;
@@ -19,38 +24,38 @@ reflow_region(void)
char *p = NULL; char *p = NULL;
char *s = NULL; char *s = NULL;
if (editor.mark_set) { if (EMARK_SET) {
if (editor.mark_cury < editor.cury || if (EMARK_CURY < ECURY ||
(editor.mark_cury == editor.cury && (EMARK_CURY == ECURY &&
editor.mark_curx < editor.curx)) { EMARK_CURX < ECURX)) {
start_row = editor.mark_cury; start_row = EMARK_CURY;
end_row = editor.cury; end_row = ECURY;
} else { } else {
start_row = editor.cury; start_row = ECURY;
end_row = editor.mark_cury; end_row = EMARK_CURY;
} }
} else { } else {
start_row = end_row = editor.cury; start_row = end_row = ECURY;
while (start_row > 0 && editor.row[start_row - 1].size > 0) { while (start_row > 0 && EROW[start_row - 1].size > 0) {
start_row--; start_row--;
} }
while (end_row < editor.nrows - 1 && while (end_row < ENROWS - 1 &&
editor.row[end_row + 1].size > 0) { EROW[end_row + 1].size > 0) {
end_row++; end_row++;
} }
} }
if (start_row >= editor.nrows) { if (start_row >= ENROWS) {
return; return;
} }
if (end_row >= editor.nrows) { if (end_row >= ENROWS) {
end_row = editor.nrows - 1; end_row = ENROWS - 1;
} }
for (i = start_row; i <= end_row; i++) { for (i = start_row; i <= end_row; i++) {
row = &editor.row[i]; row = &EROW[i];
if (row->size == 0) { if (row->size == 0) {
if (in_paragraph) { if (in_paragraph) {
@@ -64,17 +69,17 @@ reflow_region(void)
if (!in_paragraph) { if (!in_paragraph) {
indent_len = 0; indent_len = 0;
while (indent_len < row->size && while (indent_len < (int)row->size &&
(row->line[indent_len] == ' ' || (row->b[indent_len] == ' ' ||
row->line[indent_len] == '\t')) { row->b[indent_len] == '\t')) {
indent[indent_len] = row->line[indent_len], indent_len++; indent[indent_len] = row->b[indent_len], indent_len++;
} }
indent[indent_len] = '\0'; indent[indent_len] = '\0';
in_paragraph = 1; in_paragraph = 1;
} }
ab_append(&buf, row->line + indent_len, row->size - indent_len); ab_append(&buf, row->b + indent_len, row->size - indent_len);
ab_append(&buf, " ", 1); ab_append(&buf, " ", 1);
} }
@@ -157,6 +162,45 @@ reflow_region(void)
ab_free(&buf); ab_free(&buf);
editor.dirty++; EDIRTY++;
editor_set_status("Region reflowed to %d columns", REFLOW_MARGIN); editor_set_status("Region reflowed to %d columns", REFLOW_MARGIN);
} }
static inline
void clamp_curx_to_row(void)
{
abuf *row = NULL;
int maxx = 0;
if (ECURY >= ENROWS) {
return;
}
row = &EROW[ECURY];
if (ECURX < 0) {
ECURX = 0;
}
maxx = (int) row->size;
if (ECURX > maxx) {
ECURX = maxx;
}
}
static inline
void set_cursor(int col, int row)
{
if (row < 0) {
row = 0;
}
if (row > ENROWS) {
row = ENROWS;
}
ECURY = row;
ECURX = col;
clamp_curx_to_row();
}

6
term.c
View File

@@ -71,7 +71,7 @@ setup_terminal(void)
int int
get_winsz(int *rows, int *cols) get_winsz(size_t *rows, size_t *cols)
{ {
struct winsize ws; struct winsize ws;
@@ -79,8 +79,8 @@ get_winsz(int *rows, int *cols)
return -1; return -1;
} }
*cols = ws.ws_col; *cols = (size_t)ws.ws_col;
*rows = ws.ws_row; *rows = (size_t)ws.ws_row;
return 0; return 0;
} }

8
term.h
View File

@@ -1,5 +1,5 @@
#ifndef TERM_H #ifndef KE_TERM_H
#define TERM_H #define KE_TERM_H
#include "abuf.h" #include "abuf.h"
@@ -17,6 +17,6 @@ void display_clear(abuf *ab);
* on this for now because it's bloaty and this works on OpenBSD and * on this for now because it's bloaty and this works on OpenBSD and
* Linux, at least. * Linux, at least.
*/ */
int get_winsz(int *rows, int *cols); int get_winsz(size_t *rows, size_t *cols);
#endif /* TERM_H */ #endif /* KE_TERM_H */

308
undo.c
View File

@@ -1,8 +1,13 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "abuf.h" #include "abuf.h"
#include "editor.h"
#include "buffer.h"
#include "undo.h" #include "undo.h"
undo_node undo_node *
undo_node_new(undo_kind kind) undo_node_new(undo_kind kind)
{ {
undo_node *node = NULL; undo_node *node = NULL;
@@ -13,24 +18,23 @@ undo_node_new(undo_kind kind)
node->kind = kind; node->kind = kind;
node->row = node->col = 0; node->row = node->col = 0;
abuf_init(node->text); ab_init(&node->text);
node->next = NULL; node->next = NULL;
node->parent = NULL; node->parent = NULL;
return node;
} }
void void
undo_node_free(undo_node *node) undo_node_free(undo_node *node)
{ {
undo_node *next = NULL;
if (node == NULL) { if (node == NULL) {
return NULL; return;
} }
abuf_free(node-text); ab_free(&node->text);
next = node->next;
} }
@@ -44,9 +48,10 @@ undo_node_free_all(undo_node *node)
} }
while (node != NULL) { while (node != NULL) {
next = node->next;
undo_node_free(node); undo_node_free(node);
free(node); free(node);
node = node->next; node = next;
} }
} }
@@ -65,7 +70,7 @@ undo_tree_init(undo_tree *tree)
void void
undo_tree_free(undo_tree *tree) undo_tree_free(undo_tree *tree)
{ {
assert(tree == NULL); assert(tree != NULL);
undo_node_free(tree->pending); undo_node_free(tree->pending);
undo_node_free_all(tree->root); undo_node_free_all(tree->root);
@@ -86,19 +91,286 @@ undo_begin(undo_tree *tree, undo_kind kind)
undo_commit(tree); undo_commit(tree);
} }
pending = undo_new_new(kind); pending = undo_node_new(kind);
assert(pending != NULL); assert(pending != NULL);
tree->pending = pending; tree->pending = pending;
} }
void undo_prepend(abuf *buf); void
void undo_append(buf *buf); undo_prepend(undo_tree *tree, abuf *buf)
void undo_prependch(char c); {
void undo_appendch(char c); assert(tree != NULL);
void undo_commit(void); assert(tree->pending != NULL);
void undo_apply(undo_node *node);
void editor_undo(void); ab_prepend_ab(&tree->pending->text, buf);
void editor_redo(void); }
void
undo_append(undo_tree *tree, abuf *buf)
{
assert(tree != NULL);
assert(tree->pending != NULL);
ab_append_ab(&tree->pending->text, buf);
}
void
undo_prependch(undo_tree *tree, char c)
{
assert(tree != NULL);
assert(tree->pending != NULL);
ab_prependch(&tree->pending->text, c);
}
void
undo_appendch(undo_tree *tree, char c)
{
assert(tree != NULL);
assert(tree->pending != NULL);
ab_appendch(&tree->pending->text, c);
}
void
undo_commit(undo_tree *tree)
{
assert(tree != NULL);
if (tree->pending == NULL) {
return;
}
if (tree->root == NULL) {
tree->root = tree->pending;
tree->current = tree->pending;
tree->pending = NULL;
return;
}
undo_node_free_all(tree->current->next);
tree->current->next = tree->pending;
tree->pending->parent = tree->current;
tree->current = tree->pending;
tree->pending = NULL;
}
/* --- Helper functions for applying undo/redo operations --- */
static void
row_insert_at(buffer *b, size_t at, const char *s, size_t len)
{
abuf *newrows = realloc(b->row, sizeof(abuf) * (b->nrows + 1));
assert(newrows != NULL);
b->row = newrows;
if (at < b->nrows) {
memmove(&b->row[at + 1], &b->row[at], sizeof(abuf) * (b->nrows - at));
}
ab_init(&b->row[at]);
if (len > 0) {
ab_append(&b->row[at], s, len);
}
b->nrows++;
b->dirty++;
}
static void
ensure_row_exists(buffer *b, size_t r)
{
while (r >= b->nrows) {
row_insert_at(b, b->nrows, "", 0);
}
}
static void
row_insert_ch_at(abuf *row, size_t col, char ch)
{
if (col > row->size) {
col = row->size;
}
ab_resize(row, row->size + 2);
memmove(&row->b[col + 1], &row->b[col], row->size - col + 1);
row->b[col] = ch;
row->size++;
row->b[row->size] = '\0';
}
static void
row_delete_ch_at(abuf *row, size_t col)
{
if (col >= row->size) {
return;
}
memmove(&row->b[col], &row->b[col + 1], row->size - col);
row->size--;
row->b[row->size] = '\0';
}
static void
split_row_at(buffer *b, size_t r, size_t c)
{
ensure_row_exists(b, r);
abuf *row = &b->row[r];
if (c > row->size) c = row->size;
size_t rhs_len = row->size - c;
char *rhs = NULL;
if (rhs_len > 0) {
rhs = malloc(rhs_len);
assert(rhs != NULL);
memcpy(rhs, &row->b[c], rhs_len);
}
row->size = c;
if (row->cap <= row->size) {
ab_resize(row, row->size + 1);
}
row->b[row->size] = '\0';
row_insert_at(b, r + 1, rhs ? rhs : "", rhs_len);
if (rhs) free(rhs);
b->dirty++;
}
static void
join_with_next_row(buffer *b, size_t r, size_t c)
{
if (r >= b->nrows) return;
abuf *row = &b->row[r];
if (c > row->size) c = row->size;
if (r + 1 >= b->nrows) return;
abuf *next = &b->row[r + 1];
/* Make room in current row at end if needed (we append next->b after position c). */
size_t tail_len = row->size - c;
size_t add_len = next->size;
ab_resize(row, row->size + add_len + 1);
/* Move tail to make room for next content */
memmove(&row->b[c + add_len], &row->b[c], tail_len);
memcpy(&row->b[c], next->b, add_len);
row->size += add_len;
row->b[row->size] = '\0';
/* Delete next row */
ab_free(next);
if (b->nrows - (r + 2) > 0) {
memmove(&b->row[r + 1], &b->row[r + 2], sizeof(abuf) * (b->nrows - (r + 2)));
}
b->nrows--;
b->dirty++;
}
static void
buffer_insert_text(buffer *b, size_t row, size_t col, const char *s, size_t len)
{
ensure_row_exists(b, row);
size_t r = row;
size_t c = col;
if (c > b->row[r].size) c = b->row[r].size;
for (size_t i = 0; i < len; i++) {
char ch = s[i];
if (ch == '\n') {
split_row_at(b, r, c);
r++;
c = 0;
} else {
row_insert_ch_at(&b->row[r], c, ch);
c++;
}
b->dirty++;
}
}
static void
buffer_delete_text(buffer *b, size_t row, size_t col, const char *s, size_t len)
{
if (row >= b->nrows) return;
size_t r = row;
size_t c = col;
if (c > b->row[r].size) c = b->row[r].size;
for (size_t i = 0; i < len; i++) {
char ch = s[i];
if (ch == '\n') {
/* delete newline at (r,c): join row r with next */
join_with_next_row(b, r, c);
} else {
row_delete_ch_at(&b->row[r], c);
/* c stays the same because character at c is removed */
}
b->dirty++;
if (r >= b->nrows) break; /* safety */
if (c > b->row[r].size) c = b->row[r].size;
}
}
void
undo_apply(struct buffer *buf, int direction) {
undo_tree *tree = &buf->undo;
undo_node *node = NULL;
undo_commit(tree);
if (tree->root == NULL) {
return;
}
if (direction < 0) {
/* UNDO: apply inverse of current node, then move back */
node = tree->current;
if (node == NULL) {
return;
}
switch (node->kind) {
/* support insert first */
case UNDO_INSERT:
buffer_delete_text(buf, node->row, node->col,
node->text.b, node->text.size);
break;
default:
/* unknown type: do nothing */
break;
}
tree->current = node->parent; /* move back in history */
} else if (direction > 0) {
/* REDO: move forward then apply node */
undo_node *next = (tree->current == NULL)
? tree->root
: tree->current->next;
if (next == NULL) {
return; /* nothing to redo */
}
switch (next->kind) {
/* support insert first */
case UNDO_INSERT:
buffer_insert_text(buf, next->row, next->col,
next->text.b, next->text.size);
break;
default:
/* unknown type: do nothing */
break;
}
tree->current = next; /* move forward in history */
}
}
void
editor_undo(struct buffer *buf)
{
undo_apply(buf, -1);
}
void
editor_redo(struct buffer *buf)
{
undo_apply(buf, 1);
}

29
undo.h
View File

@@ -1,5 +1,14 @@
#ifndef KE_UNDO #include <stddef.h>
#define KE_UNDO
#include "abuf.h"
#include "buffer.h"
#ifndef KE_UNDO_H
#define KE_UNDO_H
struct buffer;
typedef enum undo_kind { typedef enum undo_kind {
@@ -9,7 +18,7 @@ typedef enum undo_kind {
typedef struct undo_node { typedef struct undo_node {
undo_kind op; undo_kind kind;
size_t row, col; size_t row, col;
abuf text; abuf text;
@@ -31,14 +40,14 @@ void undo_node_free(undo_node *node);
void undo_tree_init(undo_tree *tree); void undo_tree_init(undo_tree *tree);
void undo_tree_free(undo_tree *tree); void undo_tree_free(undo_tree *tree);
void undo_begin(undo_tree *tree, undo_kind kind); void undo_begin(undo_tree *tree, undo_kind kind);
void undo_prepend(abuf *buf); void undo_prepend(undo_tree *tree, abuf *buf);
void undo_append(buf *buf); void undo_append(undo_tree *tree, abuf *buf);
void undo_prependch(char c); void undo_prependch(undo_tree *tree, char c);
void undo_appendch(char c); void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree); void undo_commit(undo_tree *tree);
void undo_apply(undo_node *node); void undo_apply(struct buffer *buf, int direction);
void editor_undo(void); void editor_undo(struct buffer *buf);
void editor_redo(void); void editor_redo(struct buffer *buf);
#endif #endif

64
undo.md Normal file
View File

@@ -0,0 +1,64 @@
# Towards an Undo System
date
2025-11-27 20:14
tags
ked, hacks, text-editors
I've been thinking about building an undo system for
[ke](https://git.wntrmute.dev/kyle/ke). The first pass is going to be a
linear system. Let's start with the basic definitions for an undo
system:
``` c
typedef enum undo_kind {
UNDO_INSERT = 1 << 0,
UNDO_UNKNOWN = 1 << 1,
/* more types to follow */
} 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 {
/* the start of the undo sequence */
undo_node *root;
/* where we are currently at */
undo_node *current;
/* the current undo operations being built */
undo_node *pending;
} undo_tree;
```
The root is anchored at the last time the file was saved; when saving,
the tree is freed. Current points to the end of the history, and pending
is the sequence being built.
The lifecycle looks something like:
- `undo_tree_new` and `undo_tree_free` --- called in `init_editor` and
`deathknell`, respectively. The tree is initialized with all nodes set
to `NULL`.
- Once the user starts doing undoable things, `undo_begin(undo_kind)`
gets called, calling `undo_node_new(undo_kind)` if needed to set up
`tree->pending`. It may need to call `undo_commit` (below) if needed.
- Until an `undo_commit` is called, some form of `undo_append` or
`undo_prepend` is called.
- Finally, at some point `undo_commit` is called. This needs to do a few
things:
1. If `tree->current->next` is not `NULL`, it must be freed.
2. Set `tree->current->next` as pending, set `tree->current` to
`tree->current->next`.
3. Set `tree->pending` to `NULL`.
Once the e
- `undo_node_new(undo_kind)` and `undo_node_free`