2 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
22 changed files with 3082 additions and 2943 deletions

View File

@@ -2,9 +2,9 @@ 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.1.1") set(KE_VERSION "2.1.0")
set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g") 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")
# Optionally enable AddressSanitizer (ASan) # Optionally enable AddressSanitizer (ASan)
@@ -19,34 +19,18 @@ endif()
include(GNUInstallDirs) include(GNUInstallDirs)
set(SOURCES # Add executable
add_executable(ke
abuf.c abuf.c
core.c
term.c term.c
buffer.c buffer.c
editor.c editor.c
editing.c core.c
killring.c
process.c
undo.c
main.c
)
set(HEADERS
abuf.h
core.h core.h
term.h main.c
buffer.h
editor.h
editing.h
killring.h
process.h
undo.h undo.h
undo.c
) )
# Add executable
add_executable(ke ${SOURCES} ${HEADERS})
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)
install(FILES ke.1 TYPE MAN) install(FILES ke.1 TYPE MAN)

View File

@@ -11,10 +11,8 @@ LDFLAGS := -fsanitize=address
all: $(TARGET) test.txt all: $(TARGET) test.txt
SRCS := main.c abuf.c core.c term.c buffer.c editor.c editing.c killring.c \ SRCS := main.c abuf.c term.c buffer.c editor.c core.c
process.c undo.c HDRS := abuf.h term.h buffer.h editor.h core.h
HDRS := abuf.h core.h term.h buffer.h editor.h editing.c killring.h \
process.h undo.h
$(TARGET): $(SRCS) $(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS) $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS)

23
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"
@@ -36,6 +37,16 @@ ab_init_cap(abuf *buf, const size_t 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)
{ {
@@ -72,10 +83,10 @@ ab_append(abuf *buf, const char *s, size_t len)
void void
ab_append_ab(abuf *buf, const abuf *other) ab_append_ab(abuf *buf, abuf *other)
{ {
assert(buf != NULL); assert(buf != NULL && other != NULL);
if (other == NULL) { if (other->size == 0) {
return; return;
} }
@@ -110,10 +121,10 @@ ab_prepend(abuf *buf, const char *s, const size_t len)
void void
ab_prepend_ab(abuf *buf, const abuf *other) ab_prepend_ab(abuf *buf, abuf *other)
{ {
assert(buf != NULL); assert(buf != NULL && other != NULL);
if (other == NULL) { if (other->size == 0) {
return; return;
} }

9
abuf.h
View File

@@ -19,13 +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_append_ab(abuf *buf, const abuf *other); void ab_append_ab(abuf *buf, abuf *other);
void ab_prependch(abuf *buf, const char c); void ab_prependch(abuf *buf, char c);
void ab_prepend(abuf *buf, const char *s, const size_t len); void ab_prepend(abuf *buf, const char *s, size_t len);
void ab_prepend_ab(abuf *buf, const abuf *other); void ab_prepend_ab(abuf *buf, abuf *other);
void ab_free(abuf *buf); void ab_free(abuf *buf);

View File

@@ -240,8 +240,7 @@ buffer_list_resize(void)
buffer **newlist = NULL; buffer **newlist = NULL;
if (editor.bufcount == editor.bufcap) { if (editor.bufcount == editor.bufcap) {
editor.bufcap = (size_t)cap_growth((int)editor.bufcap, editor.bufcap = (size_t)cap_growth((int)editor.bufcap, (int)editor.bufcount + 1);
(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);
@@ -273,7 +272,7 @@ 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); undo_tree_init(&buf->undo);
editor.buffers[editor.bufcount] = buf; editor.buffers[editor.bufcount] = buf;
@@ -283,13 +282,20 @@ buffer_add_empty(void)
} }
void
buffer_save_current(void)
{
/* No-op: editor no longer mirrors per-buffer fields */
(void)editor;
}
buffer * buffer *
buffer_current(void) buffer_current(void)
{ {
if (editor.bufcount == 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];
} }
@@ -395,6 +401,8 @@ 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 (size_t 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]);
@@ -460,4 +468,3 @@ buffer_switch_by_name(void)
free(name); free(name);
} }

View File

@@ -18,8 +18,9 @@ typedef struct buffer {
undo_tree undo; undo_tree undo;
} buffer; } buffer;
/* Access current buffer and convenient aliases for file-specific fields */
buffer *buffer_current(void); buffer *buffer_current(void);
#define CURBUF (buffer_current()) #define CURBUF (buffer_current())
#define EROW (CURBUF->row) #define EROW (CURBUF->row)
#define ENROWS (CURBUF->nrows) #define ENROWS (CURBUF->nrows)

38
core.c
View File

@@ -1,4 +1,3 @@
#include <sys/stat.h>
#include <assert.h> #include <assert.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
@@ -8,9 +7,7 @@
#include "core.h" #include "core.h"
#ifdef INCLUDE_STRNSTR #ifdef INCLUDE_STRNSTR
/* /*
* Find the first occurrence of find in s, where the search is limited to the * Find the first occurrence of find in s, where the search is limited to the
* first slen characters of s. * first slen characters of s.
@@ -38,40 +35,6 @@ strnstr(const char *s, const char *find, size_t slen)
#endif #endif
int
path_is_dir(const char *path)
{
struct stat st;
if (path == NULL) {
return 0;
}
if (stat(path, &st) == 0) {
return S_ISDIR(st.st_mode);
}
return 0;
}
size_t
str_lcp2(const char *a, const char *b)
{
size_t i = 0;
if (!a || !b) {
return 0;
}
while (a[i] && b[i] && a[i] == b[i]) {
i++;
}
return i;
}
void void
swap_size_t(size_t *first, size_t *second) swap_size_t(size_t *first, size_t *second)
{ {
@@ -148,4 +111,3 @@ die(const char* s)
perror(s); perror(s);
exit(1); exit(1);
} }

13
core.h
View File

@@ -4,17 +4,6 @@
#include <stddef.h> #include <stddef.h>
#ifndef KE_VERSION
#define KE_VERSION "ke dev build"
#endif
#define ESCSEQ "\x1b["
#define CTRL_KEY(key) ((key)&0x1f)
#define TAB_STOP 8
#define MSG_TIMEO 3
#define TAB_STOP 8
#define INITIAL_CAPACITY 8 #define INITIAL_CAPACITY 8
@@ -39,8 +28,6 @@ char *strnstr(const char *s, const char *find, size_t slen);
#define INCLUDE_STRNSTR #define INCLUDE_STRNSTR
#endif #endif
int path_is_dir(const char *path);
size_t str_lcp2(const char *a, const char *b);
void swap_size_t(size_t *first, size_t *second); void swap_size_t(size_t *first, size_t *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);

1574
editing.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
#ifndef KE_EDITING_H
#define KE_EDITING_H
#include <stdint.h>
#include "abuf.h"
/* miscellaneous */
void file_open_prompt_cb(char *buf, int16_t key);
int erow_render_to_cursor(const abuf *row, int cx);
int erow_cursor_to_render(abuf *row, int rx);
int erow_init(abuf *row, int len);
void erow_insert(int at, char *s, int len);
void jump_to_position(size_t col, size_t row);
void goto_line(void);
int cursor_at_eol(void);
int iswordchar(unsigned char c);
void find_next_word(void);
void delete_next_word(void);
void find_prev_word(void);
void delete_prev_word(void);
void delete_row(size_t at);
void row_append_row(abuf *row, const char *s, int len);
void row_insert_ch(abuf *row, int at, int16_t c);
void row_delete_ch(abuf *row, int at);
void insertch(int16_t c);
void deletech(uint8_t op);
void open_file(const char *filename);
char *rows_to_buffer(int *buflen);
int save_file(void);
uint16_t is_arrow_key(int16_t c);
int16_t get_keypress(void);
char *editor_prompt(const char*, void (*cb)(char*, int16_t));
void editor_find_callback(char *query, int16_t c);
void editor_find(void);
void editor_openfile(void);
int first_nonwhitespace(abuf *row);
void move_cursor_once(int16_t c, int interactive);
void move_cursor(int16_t c, int interactive);
void uarg_start(void);
void uarg_digit(int d);
void uarg_clear(void);
int uarg_get(void);
void newline(void);
char *get_cloc_code_lines(const char *filename);
int dump_pidfile(void);
#endif

View File

@@ -79,6 +79,7 @@ init_editor(void)
void void
reset_editor(void) reset_editor(void)
{ {
/* Reset the current buffer's contents/state. */
buffer *b = buffer_current(); buffer *b = buffer_current();
if (b == NULL) { if (b == NULL) {
return; return;
@@ -90,7 +91,6 @@ reset_editor(void)
} }
free(b->row); free(b->row);
} }
b->row = NULL; b->row = NULL;
b->nrows = 0; b->nrows = 0;
b->rowoffs = 0; b->rowoffs = 0;
@@ -107,4 +107,3 @@ reset_editor(void)
b->mark_curx = 0; b->mark_curx = 0;
b->mark_cury = 0; b->mark_cury = 0;
} }

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

View File

@@ -1,356 +0,0 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "abuf.h"
#include "core.h"
#include "editing.h"
#include "editor.h"
#include "killring.h"
void
killring_flush(void)
{
if (editor.killring != NULL) {
ab_free(editor.killring);
free(editor.killring);
editor.killring = NULL;
}
}
void
killring_yank(void)
{
if (editor.killring == NULL) {
return;
}
/*
* Insert killring contents at the cursor without clearing the ring.
* Interpret '\n' as an actual newline() rather than inserting a raw 0x0A
* byte, so yanked content preserves lines correctly.
*/
for (int i = 0; i < (int)editor.killring->size; i++) {
unsigned char ch = (unsigned char)editor.killring->b[i];
if (ch == '\n') {
newline();
} else {
insertch(ch);
}
}
}
void
killring_start_with_char(unsigned char ch)
{
abuf *row = NULL;
if (editor.killring != NULL) {
ab_free(editor.killring);
free(editor.killring);
editor.killring = NULL;
}
editor.killring = malloc(sizeof(abuf));
assert(editor.killring != NULL);
assert(erow_init(editor.killring, 0) == 0);
/* append one char to empty killring without affecting editor.dirty */
row = editor.killring;
row->b = realloc(row->b, row->size + 2);
assert(row->b != NULL);
row->b[row->size] = ch;
row->size++;
row->b[row->size] = '\0';
}
void
killring_append_char(unsigned char ch)
{
abuf *row = NULL;
if (editor.killring == NULL) {
killring_start_with_char(ch);
return;
}
row = editor.killring;
row->b = realloc(row->b, row->size + 2);
assert(row->b != NULL);
row->b[row->size] = ch;
row->size++;
row->b[row->size] = '\0';
}
void
killring_prepend_char(unsigned char ch)
{
abuf *row = NULL;
if (editor.killring == NULL) {
killring_start_with_char(ch);
return;
}
row = editor.killring;
row->b = realloc(row->b, row->size + 2);
assert(row->b != NULL);
memmove(&row->b[1], &row->b[0], row->size + 1);
row->b[0] = ch;
row->size++;
}
void
toggle_markset(void)
{
if (EMARK_SET) {
EMARK_SET = 0;
editor_set_status("Mark cleared.");
return;
}
EMARK_SET = 1;
EMARK_CURX = ECURX;
EMARK_CURY = ECURY;
editor_set_status("Mark set.");
}
int
cursor_after_mark(void)
{
if (EMARK_CURY < ECURY) {
return 1;
}
if (EMARK_CURY > ECURY) {
return 0;
}
return ECURX >= EMARK_CURX;
}
int
count_chars_from_cursor_to_mark(void)
{
size_t count = 0;
size_t curx = ECURX;
size_t cury = ECURY;
size_t markx = EMARK_CURX;
size_t marky = EMARK_CURY;
if (!cursor_after_mark()) {
swap_size_t(&curx, &markx);
swap_size_t(&curx, &marky);
}
ECURX = markx;
ECURY = marky;
while (ECURY != cury) {
while (!cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
count++;
}
move_cursor(ARROW_RIGHT, 1);
count++;
}
while (ECURX != curx) {
count++;
move_cursor(ARROW_RIGHT, 1);
}
return count;
}
void
kill_region(void)
{
size_t curx = ECURX;
size_t cury = ECURY;
size_t markx = EMARK_CURX;
size_t marky = EMARK_CURY;
if (!EMARK_SET) {
return;
}
/* kill the current killring first */
killring_flush();
if (!cursor_after_mark()) {
swap_size_t(&curx, &markx);
swap_size_t(&cury, &marky);
}
ECURX = markx;
ECURY = marky;
while (ECURY != cury) {
while (!cursor_at_eol()) {
killring_append_char(EROW[ECURY].b[ECURX]);
move_cursor(ARROW_RIGHT, 0);
}
killring_append_char('\n');
move_cursor(ARROW_RIGHT, 0);
}
while (ECURX != curx) {
killring_append_char(EROW[ECURY].b[ECURX]);
move_cursor(ARROW_RIGHT, 0);
}
editor_set_status("Region killed.");
/* clearing the mark needs to be done outside this function; *
* when deleting the region, the mark needs to be set too. */
}
void
indent_region(void)
{
size_t start_row = 0;
size_t end_row = 0;
size_t i = 0;
if (!EMARK_SET) {
return;
}
if (EMARK_CURY < ECURY) {
start_row = EMARK_CURY;
end_row = ECURY;
} else if (EMARK_CURY > ECURY) {
start_row = ECURY;
end_row = EMARK_CURY;
} else {
start_row = end_row = ECURY;
}
/* Ensure bounds are valid */
if (end_row >= ENROWS) {
end_row = ENROWS - 1;
}
if (start_row >= ENROWS) {
return;
}
for (i = start_row; i <= end_row; i++) {
row_insert_ch(&EROW[i], 0, '\t');
}
ECURX = 0;
EDIRTY++;
}
void
unindent_region(void)
{
size_t start_row = 0;
size_t end_row = 0;
size_t i = 0;
size_t del = 0;
abuf *row = NULL;
if (!EMARK_SET) {
editor_set_status("Mark not set.");
return;
}
if (EMARK_CURY < ECURY ||
(EMARK_CURY == ECURY && EMARK_CURX < ECURX)) {
start_row = EMARK_CURY;
end_row = ECURY;
} else {
start_row = ECURY;
end_row = EMARK_CURY;
}
if (start_row >= ENROWS) {
return;
}
if (end_row >= ENROWS) {
end_row = ENROWS - 1;
}
for (i = start_row; i <= end_row; i++) {
row = &EROW[i];
if (row->size == 0) {
continue;
}
if (row->b[0] == '\t') {
row_delete_ch(row, 0);
} else if (row->b[0] == ' ') {
del = 0;
while (del < TAB_STOP && del < row->size &&
row->b[del] == ' ') {
del++;
}
if (del > 0) {
/* +1 for NUL */
memmove(row->b, row->b + del,
row->size - del + 1);
row->size -= del;
}
}
}
ECURX = 0;
ECURY = start_row;
EDIRTY++;
editor_set_status("Region unindented");
}
void
delete_region(void)
{
size_t count = count_chars_from_cursor_to_mark();
size_t killed = 0;
size_t curx = ECURX;
size_t cury = ECURY;
size_t markx = EMARK_CURX;
size_t marky = EMARK_CURY;
if (!EMARK_SET) {
return;
}
if (!cursor_after_mark()) {
swap_size_t(&curx, &markx);
swap_size_t(&cury, &marky);
}
jump_to_position(markx, marky);
while (killed < count) {
move_cursor(ARROW_RIGHT, 0);
deletech(KILLRING_NO_OP);
killed++;
}
while (ECURX != markx && ECURY != marky) {
deletech(KILLRING_NO_OP);
}
editor.kill = 1;
editor_set_status("Region killed.");
}

View File

@@ -1,26 +0,0 @@
#ifndef KE_KILLRING_H
#define KE_KILLRING_H
#define KILLRING_NO_OP 0 /* don't touch the killring */
#define KILLRING_APPEND 1 /* append deleted chars */
#define KILLRING_PREPEND 2 /* prepend deleted chars */
#define KILLING_SET 3 /* set killring to deleted char */
#define KILLRING_FLUSH 4 /* clear the killring */
void killring_flush(void);
void killring_yank(void);
void killring_start_with_char(unsigned char ch);
void killring_append_char(unsigned char ch);
void killring_prepend_char(unsigned char ch);
void toggle_markset(void);
int cursor_after_mark(void);
int count_chars_from_cursor_to_mark(void);
void kill_region(void);
void indent_region(void);
void unindent_region(void);
void delete_region(void);
#endif

2696
main.c

File diff suppressed because it is too large Load Diff

438
process.c
View File

@@ -1,438 +0,0 @@
#include <errno.h>
#include <ctype.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <_string.h>
#include "editing.h"
#include "buffer.h"
#include "core.h"
#include "editor.h"
#include "killring.h"
#include "term.h"
#include "process.h"
void
process_kcommand(const int16_t c)
{
char *buf = NULL;
size_t len = 0;
int jumpx = 0;
int jumpy = 0;
int reps = 0;
switch (c) {
case BACKSPACE:
while (ECURX > 0) {
process_normal(BACKSPACE);
}
break;
case '=':
if (EMARK_SET) {
indent_region();
} else {
editor_set_status("Mark not set.");
}
break;
case '-':
if (EMARK_SET) {
unindent_region();
} else {
editor_set_status("Mark not set.");
}
break;
case CTRL_KEY('\\'):
/* sometimes it's nice to dump core */
disable_termraw();
abort();
case '@':
if (!dump_pidfile()) {
break;
}
/* FALLTHRU */
case '!':
/* useful for debugging */
editor_set_status("PID: %ld", (long) getpid());
break;
case ' ':
toggle_markset();
break;
case CTRL_KEY(' '):
jumpx = EMARK_CURX;
jumpy = EMARK_CURY;
EMARK_CURX = ECURX;
EMARK_CURY = ECURY;
jump_to_position(jumpx, jumpy);
editor_set_status("Jumped to mark");
break;
case 'c':
buffer_close_current();
break;
case 'd':
if (ECURX == 0 && cursor_at_eol()) {
delete_row(ECURY);
return;
}
reps = uarg_get();
while (reps--) {
while ((EROW[ECURY].size - ECURX) > 0) {
process_normal(DEL_KEY);
}
if (reps) {
newline();
}
}
break;
case DEL_KEY:
case CTRL_KEY('d'):
reps = uarg_get();
while (reps--) {
delete_row(ECURY);
}
break;
case 'e':
case CTRL_KEY('e'):
if (EDIRTY && editor.dirtyex) {
editor_set_status(
"File not saved - C-k e again to open a new file anyways.");
editor.dirtyex = 0;
return;
}
editor_openfile();
break;
case 'f':
if (editor.killring == NULL || editor.killring->size == 0) {
editor_set_status("The kill ring is empty.");
break;
}
len = editor.killring ? editor.killring->size : 0;
killring_flush();
editor_set_status("Kill ring cleared (%lu characters)", len);
break;
case 'n':
buffer_next();
break;
case 'p':
buffer_prev();
break;
case 'b':
buffer_switch_by_name();
break;
case 'g':
goto_line();
break;
case 'j':
if (!EMARK_SET) {
editor_set_status("Mark not set.");
break;
}
jumpx = EMARK_CURX;
jumpy = EMARK_CURY;
EMARK_CURX = ECURX;
EMARK_CURY = ECURY;
jump_to_position(jumpx, jumpy);
editor_set_status("Jumped to mark; mark is now the previous location.");
break;
case 'l':
buf = get_cloc_code_lines(EFILENAME);
editor_set_status("Lines of code: %s", buf);
free(buf);
break;
case 'm':
/* todo: fix the process failed: success issue */
if (system("make") != 0) {
editor_set_status(
"process failed: %s",
strerror(errno));
} else {
editor_set_status("make: ok");
}
break;
case 'q':
if (EDIRTY && editor.dirtyex) {
editor_set_status(
"File not saved - C-k q again to quit.");
editor.dirtyex = 0;
return;
}
exit(0);
case CTRL_KEY('q'):
exit(0);
case CTRL_KEY('r'):
if (EDIRTY && editor.dirtyex) {
editor_set_status("File not saved - C-k C-r again to reload.");
editor.dirtyex = 0;
return;
}
jumpx = ECURX;
jumpy = ECURY;
buf = strdup(EFILENAME);
reset_editor();
open_file(buf);
display_refresh();
free(buf);
jump_to_position(jumpx, jumpy);
editor_set_status("file reloaded");
break;
case CTRL_KEY('s'):
case 's':
save_file();
break;
case CTRL_KEY('x'):
case 'x':
exit(save_file());
case 'u':
reps = uarg_get();
while (reps--) {}
editor_set_status("Undo not implemented.");
break;
case 'U':
reps = uarg_get();
while (reps--) {}
editor_set_status("Redo not implemented.");
break;
case 'y':
reps = uarg_get();
while (reps--) {
killring_yank();
}
break;
case ESC_KEY:
case CTRL_KEY('g'):
break;
default:
if (isprint(c)) {
editor_set_status("unknown kcommand '%c'", c);
break;
}
editor_set_status("unknown kcommand: %04x", c);
return;
}
editor.dirtyex = 1;
}
void
process_normal(int16_t c)
{
size_t cols = 0;
size_t rows = 0;
int reps = 0;
/* C-u handling must be the very first thing */
if (c == CTRL_KEY('u')) {
uarg_start();
return;
}
/* digits after a C-u are part of the argument */
if (editor.uarg && c >= '0' && c <= '9') {
uarg_digit(c - '0');
return;
}
if (is_arrow_key(c)) {
/* moving the cursor breaks a delete sequence */
editor.kill = 0;
move_cursor(c, 1);
return;
}
switch (c) {
case '\r':
newline();
break;
case CTRL_KEY('k'):
editor.mode = MODE_KCOMMAND;
return;
case BACKSPACE:
case CTRL_KEY('h'):
case CTRL_KEY('d'):
case DEL_KEY:
if (c == DEL_KEY || c == CTRL_KEY('d')) {
reps = uarg_get();
while (reps-- > 0) {
move_cursor(ARROW_RIGHT, 1);
deletech(KILLRING_APPEND);
}
} else {
reps = uarg_get();
while (reps-- > 0) {
deletech(KILLRING_PREPEND);
}
}
break;
case CTRL_KEY('a'): /* beginning of line */
case HOME_KEY:
move_cursor(CTRL_KEY('a'), 1);
break;
case CTRL_KEY('e'): /* end of line */
case END_KEY:
move_cursor(CTRL_KEY('e'), 1);
break;
case CTRL_KEY('g'):
break;
case CTRL_KEY('l'):
if (get_winsz(&rows, &cols) == 0) {
editor.rows = rows;
editor.cols = cols;
} else {
editor_set_status("Couldn't update window size.");
}
display_refresh();
break;
case CTRL_KEY('s'):
editor_find();
break;
case CTRL_KEY('w'):
kill_region();
delete_region();
toggle_markset();
break;
case CTRL_KEY('y'):
reps = uarg_get();
while (reps-- > 0) {
killring_yank();
}
break;
case ESC_KEY:
editor.mode = MODE_ESCAPE;
break;
default:
if (c == TAB_KEY) {
reps = uarg_get();
while (reps-- > 0) {
insertch(c);
}
} else if (c >= 0x20 && c != 0x7f) {
reps = uarg_get();
while (reps-- > 0) {
insertch(c);
}
}
break;
}
editor.dirtyex = 1;
}
void
process_escape(const int16_t c)
{
int reps = 0;
editor_set_status("hi");
switch (c) {
case '>':
ECURY = ENROWS;
ECURX = 0;
break;
case '<':
ECURY = 0;
ECURX = 0;
break;
case 'b':
reps = uarg_get();
while (reps--) {
find_prev_word();
}
break;
case 'd':
reps = uarg_get();
while (reps--) {
delete_next_word();
}
break;
case 'f':
reps = uarg_get();
while (reps--) {
find_next_word();
}
break;
case 'm':
toggle_markset();
break;
case 'w':
if (!EMARK_SET) {
editor_set_status("mark isn't set");
break;
}
kill_region();
toggle_markset();
break;
case BACKSPACE:
reps = uarg_get();
while (reps--) {
delete_prev_word();
}
break;
case ESC_KEY:
case CTRL_KEY('g'):
break; /* escape from escape-mode the movie */
default:
editor_set_status("unknown ESC key: %04x", c);
}
uarg_clear();
}
int
process_keypress(void)
{
const int16_t c = get_keypress();
if (c <= 0) {
return 0;
}
switch (editor.mode) {
case MODE_KCOMMAND:
process_kcommand(c);
editor.mode = MODE_NORMAL;
break;
case MODE_NORMAL:
process_normal(c);
break;
case MODE_ESCAPE:
process_escape(c);
editor.mode = MODE_NORMAL;
break;
default:
editor_set_status("we're in the %d-D space now cap'n",
editor.mode);
editor.mode = MODE_NORMAL;
}
return 1;
}

View File

@@ -1,22 +0,0 @@
#ifndef KE_PROCESS_H
#define KE_PROCESS_H
#include <stdint.h>
/*
* define the keyboard input modes
* normal: no special mode
* kcommand: ^k commands
* escape: what happens when you hit escape?
*/
#define MODE_NORMAL 0
#define MODE_KCOMMAND 1
#define MODE_ESCAPE 2
void process_kcommand(int16_t c);
void process_normal(int16_t c);
void process_escape(int16_t c);
int process_keypress(void);
#endif

228
term.c
View File

@@ -1,18 +1,13 @@
#include <sys/ioctl.h>
#include <assert.h> #include <assert.h>
#include <errno.h> #include <errno.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include <sys/ioctl.h>
#include "abuf.h" #include "abuf.h"
#include "core.h" #include "core.h"
#include "buffer.h"
#include "editing.h"
#include "editor.h"
#include "process.h"
#include "term.h" #include "term.h"
#define ESCSEQ "\x1b[" #define ESCSEQ "\x1b["
@@ -89,224 +84,3 @@ get_winsz(size_t *rows, size_t *cols)
return 0; return 0;
} }
void
draw_rows(abuf *ab)
{
abuf *row = NULL;
char buf[editor.cols];
char c = 0;
size_t j = 0;
size_t filerow = 0;
size_t y = 0;
size_t len = 0;
size_t padding = 0;
size_t printed = 0;
size_t rx = 0;
for (y = 0; y < editor.rows; y++) {
filerow = y + EROWOFFS;
if (filerow >= ENROWS) {
if ((ENROWS == 0) && (y == editor.rows / 3)) {
len = snprintf(buf,
sizeof(buf),
"%s",
KE_VERSION);
padding = (editor.rows - len) / 2;
if (padding) {
ab_append(ab, "|", 1);
padding--;
}
while (padding--)
ab_append(ab, " ", 1);
ab_append(ab, buf, len);
} else {
ab_append(ab, "|", 1);
}
} else {
row = &EROW[filerow];
j = 0;
rx = printed = 0;
while (j < row->size && printed < editor.cols) {
c = row->b[j];
if (rx < ECOLOFFS) {
if (c == '\t') rx += (TAB_STOP - (rx % TAB_STOP));
else if (c < 0x20) rx += 3;
else rx++;
j++;
continue;
}
if (c == '\t') {
int sp = TAB_STOP - (rx % TAB_STOP);
for (int k = 0; k < sp && printed < editor.cols; k++) {
ab_appendch(ab, ' ');
printed++;
rx++;
}
} else if (c < 0x20) {
char seq[4];
snprintf(seq, sizeof(seq), "\\%02x", c);
ab_append(ab, seq, 3);
printed += 3;
rx += 3;
} else {
ab_appendch(ab, c);
printed++;
rx++;
}
j++;
}
len = printed;
}
ab_append(ab, ESCSEQ "K", 3);
ab_append(ab, "\r\n", 2);
}
}
char
status_mode_char(void)
{
switch (editor.mode) {
case MODE_NORMAL:
return 'N';
case MODE_KCOMMAND:
return 'K';
case MODE_ESCAPE:
return 'E';
default:
return '?';
}
}
void
draw_status_bar(abuf *ab)
{
char status[editor.cols];
char rstatus[editor.cols];
char mstatus[editor.cols];
size_t len = 0;
size_t rlen = 0;
len = snprintf(status,
sizeof(status),
"%c%cke: %.20s - %lu lines",
status_mode_char(),
EDIRTY ? '!' : '-',
EFILENAME ? EFILENAME : "[no file]",
ENROWS);
if (EMARK_SET) {
snprintf(mstatus,
sizeof(mstatus),
" | M: %lu, %lu ",
EMARK_CURX + 1,
EMARK_CURY + 1);
} else {
snprintf(mstatus, sizeof(mstatus), " | M:clear ");
}
rlen = snprintf(rstatus,
sizeof(rstatus),
"L%lu/%lu C%lu %s",
ECURY + 1,
ENROWS,
ECURX + 1,
mstatus);
ab_append(ab, ESCSEQ "7m", 4);
ab_append(ab, status, len);
while (len < editor.cols) {
if (editor.cols - len == rlen) {
ab_append(ab, rstatus, rlen);
break;
}
ab_append(ab, " ", 1);
len++;
}
ab_append(ab, ESCSEQ "m", 3);
ab_append(ab, "\r\n", 2);
}
void
draw_message_line(abuf *ab)
{
size_t len = strlen(editor.msg);
ab_append(ab, ESCSEQ "K", 3);
if (len > editor.cols) {
len = editor.cols;
}
if (len && time(NULL) - editor.msgtm < MSG_TIMEO) {
ab_append(ab, editor.msg, len);
}
}
void
scroll(void)
{
const abuf *row = NULL;
ERX = 0;
if (ECURY < ENROWS) {
row = &EROW[ECURY];
ERX = erow_render_to_cursor(row, ECURX);
}
if (ECURY < EROWOFFS) {
EROWOFFS = ECURY;
}
if (ECURY >= EROWOFFS + editor.rows) {
EROWOFFS = ECURY - editor.rows + 1;
}
if (ERX < ECOLOFFS) {
ECOLOFFS = ERX;
}
if (ERX >= ECOLOFFS + editor.cols) {
ECOLOFFS = ERX - editor.cols + 1;
}
}
void
display_refresh(void)
{
char buf[32] = {0};
abuf ab = ABUF_INIT;
scroll();
ab_append(&ab, ESCSEQ "?25l", 6);
ab_append(&ab, ESCSEQ "H", 3);
display_clear(&ab);
draw_rows(&ab);
draw_status_bar(&ab);
draw_message_line(&ab);
snprintf(buf,
sizeof(buf),
ESCSEQ "%lu;%luH",
(ECURY - EROWOFFS) + 1,
(ERX - ECOLOFFS) + 1);
ab_append(&ab, buf, kstrnlen(buf, 32));
/* ab_append(&ab, ESCSEQ "1;2H", 7); */
ab_append(&ab, ESCSEQ "?25h", 6);
kwrite(STDOUT_FILENO, ab.b, (int)ab.size);
ab_free(&ab);
}

8
term.h
View File

@@ -3,18 +3,11 @@
#include "abuf.h" #include "abuf.h"
/* Terminal control/setup API */ /* Terminal control/setup API */
void enable_termraw(void); void enable_termraw(void);
void disable_termraw(void); void disable_termraw(void);
void setup_terminal(void); void setup_terminal(void);
void display_clear(abuf *ab); void display_clear(abuf *ab);
void draw_rows(abuf *ab);
char status_mode_char(void);
void draw_status_bar(abuf *ab);
void draw_message_line(abuf *ab);
void scroll(void);
void display_refresh(void);
/* /*
* get_winsz uses the TIOCGWINSZ to get the window size. * get_winsz uses the TIOCGWINSZ to get the window size.
@@ -26,5 +19,4 @@ void display_refresh(void);
*/ */
int get_winsz(size_t *rows, size_t *cols); int get_winsz(size_t *rows, size_t *cols);
#endif /* KE_TERM_H */ #endif /* KE_TERM_H */

256
undo.c
View File

@@ -1,11 +1,8 @@
/*
* undo.c: ke's undo system
*/
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "abuf.h" #include "abuf.h"
#include "editor.h"
#include "buffer.h" #include "buffer.h"
#include "undo.h" #include "undo.h"
@@ -24,7 +21,7 @@ undo_node_new(undo_kind kind)
ab_init(&node->text); ab_init(&node->text);
node->next = NULL; node->next = NULL;
node->next = NULL; node->parent = NULL;
return node; return node;
} }
@@ -151,90 +148,229 @@ undo_commit(undo_tree *tree)
} }
if (tree->root == NULL) { if (tree->root == NULL) {
assert(tree->current == NULL);
tree->root = tree->pending; tree->root = tree->pending;
tree->current = tree->pending; tree->current = tree->pending;
tree->pending = NULL; tree->pending = NULL;
return; return;
} }
assert(tree->current != NULL);
if (tree->current->next != NULL) {
undo_node_free_all(tree->current->next); undo_node_free_all(tree->current->next);
}
tree->pending->prev = tree->current;
tree->current->next = tree->pending; tree->current->next = tree->pending;
tree->pending->parent = tree->current;
tree->current = tree->pending; tree->current = tree->pending;
tree->pending = NULL; tree->pending = NULL;
} }
static int /* --- Helper functions for applying undo/redo operations --- */
undo_apply_undo(struct buffer *buf) static void
row_insert_at(buffer *b, size_t at, const char *s, size_t len)
{ {
undo_node *node = buf->undo.current; 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) { switch (node->kind) {
/* support insert first */
case UNDO_INSERT: case UNDO_INSERT:
buffer_delete_text(buf, node->row, node->col,
node->text.b, node->text.size);
break; break;
default: default:
return 0; /* unknown type: do nothing */
break;
} }
return 0; 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 */
}
} }
int void
undo_apply(struct buffer *buf, int direction)
{
(void)direction;
if (buf == NULL) {
return 0;
}
undo_commit(&buf->undo);
if (buf->undo.current == NULL) {
return 0;
}
if (direction == UNDO_DIR_UNDO) {
return undo_apply_undo(buf);
} else if (direction == UNDO_DIR_UNDO) {
return 0;
} else {
return 0;
}
return 0;
}
int
editor_undo(struct buffer *buf) editor_undo(struct buffer *buf)
{ {
if (buf == NULL) { undo_apply(buf, -1);
return 0;
}
undo_commit(&buf->undo);
return undo_apply(buf, UNDO_DIR_UNDO);
} }
int void
editor_redo(struct buffer *buf) editor_redo(struct buffer *buf)
{ {
if (buf == NULL) { undo_apply(buf, 1);
return 0;
}
undo_commit(&buf->undo);
return undo_apply(buf, UNDO_DIR_REDO);
} }

13
undo.h
View File

@@ -1,6 +1,7 @@
#include <stddef.h> #include <stddef.h>
#include "abuf.h" #include "abuf.h"
#include "buffer.h"
#ifndef KE_UNDO_H #ifndef KE_UNDO_H
@@ -10,10 +11,6 @@
struct buffer; struct buffer;
#define UNDO_DIR_UNDO -1
#define UNDO_DIR_REDO 1
typedef enum undo_kind { typedef enum undo_kind {
UNDO_INSERT = 1 << 0, UNDO_INSERT = 1 << 0,
UNDO_UNKNOWN = 1 << 1, UNDO_UNKNOWN = 1 << 1,
@@ -26,7 +23,7 @@ typedef struct undo_node {
abuf text; abuf text;
struct undo_node *next; struct undo_node *next;
struct undo_node *prev; struct undo_node *parent;
} undo_node; } undo_node;
@@ -48,9 +45,9 @@ void undo_append(undo_tree *tree, abuf *buf);
void undo_prependch(undo_tree *tree, char c); void undo_prependch(undo_tree *tree, char c);
void undo_appendch(undo_tree *tree, char c); void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree); void undo_commit(undo_tree *tree);
int undo_apply(struct buffer *buf, int direction); void undo_apply(struct buffer *buf, int direction);
int editor_undo(struct buffer *buf); void editor_undo(struct buffer *buf);
int editor_redo(struct buffer *buf); 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`