10 Commits

Author SHA1 Message Date
db38266849 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-26 16:09:43 -08:00
6d1b7f8e56 keep default.nix in sync 2025-11-26 16:08:38 -08:00
64647f77b0 performance improvements 2025-11-26 16:00:15 -08:00
2c3b2ae0f0 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-26 15:25:37 -08:00
a605e47458 kbhit makes it go a lot faster 2025-11-26 15:25:32 -08:00
40ee1e8d7b kill unused declaration. 2025-11-26 13:46:48 -08:00
4464159301 remove all undo stuff to start over 2025-11-26 13:45:44 -08:00
dc9fb58a41 remove all undo stuff to start over 2025-11-26 13:40:08 -08:00
bbd682cec7 Remove and start over with some of the undo stuff. 2025-11-26 08:25:41 -08:00
ace25c5c65 more undo work 2025-11-26 03:00:49 -08:00
3 changed files with 114 additions and 493 deletions

View File

@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15)
project(ke C) # Specify C language explicitly
set(CMAKE_C_STANDARD 99)
set(KE_VERSION "1.5.0")
set(KE_VERSION "1.5.2")
set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE")

View File

@@ -5,9 +5,17 @@
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
# Find the line containing set(KE_VERSION "...")
versionLine = lib.findFirst (l: builtins.match ".*set\\(KE_VERSION \".+\"\\).*" l != null) (throw "KE_VERSION not found in CMakeLists.txt") cmakeLines;
# Extract the version number
version = builtins.head (builtins.match ".*set\\(KE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "ke";
version = "1.5.0";
inherit version;
src = lib.cleanSource ./.;

595
main.c
View File

@@ -88,45 +88,17 @@ struct abuf {
/* editor row */
struct erow {
char *line;
char *render;
char *line;
char *render;
int size;
int rsize;
int size;
int rsize;
int cap;
int cap;
int dirty;
};
typedef enum undo_flag {
UNDO_INSERT = 1 << 0, /* insertch */
UNDO_DELETE = 1 << 1, /* deletech */
UNDO_PASTE = 1 << 2, /* yank */
UNDO_NEWLINE = 1 << 3, /* newline duh */
UNDO_DELETE_ROW = 1 << 4, /* delete_row duh */
UNDO_INDENT = 1 << 5,
UNDO_UNINDENT = 1 << 6,
UNDO_KILL_REGION = 1 << 7
} undo_flag_t;
typedef struct undo_node {
undo_flag_t type;
int row, col;
struct abuf text;
struct undo_node *next;
struct undo_node *child;
} undo_node_t;
typedef struct undo_tree {
undo_node_t *root;
undo_node_t *current;
undo_node_t *saved;
undo_node_t *pending;
} undo_tree_t;
/*
* editor is the global editor state; it should be broken out
* to buffers and screen state, probably.
@@ -151,7 +123,6 @@ struct editor_t {
int mark_curx, mark_cury;
int uarg, ucount; /* C-u support */
time_t msgtm;
undo_tree_t *undo;
} editor = {
.cols = 0,
.rows = 0,
@@ -173,7 +144,6 @@ struct editor_t {
.mark_cury = 0,
.uarg = 0,
.ucount = 0,
.undo = NULL,
};
@@ -187,6 +157,8 @@ size_t kstrnlen(const char *buf, const size_t max);
void ab_init(struct abuf *buf);
void ab_appendch(struct abuf *buf, char c);
void ab_append(struct abuf *buf, const char *s, size_t len);
void ab_prependch(struct abuf *buf, char c);
void ab_prepend(struct abuf *buf, const char *s, size_t len);
void ab_free(struct abuf *buf);
char nibble_to_hex(char c);
void swap_int(int *a, int *b);
@@ -199,67 +171,6 @@ void erow_update(struct erow *row);
void erow_insert(int at, char *s, int len);
void erow_free(struct erow *row);
/*
* undo ops
*
* notes:
* + undo_node_free destroys an entire timeline, including children and next.
* + undo_node_free_branch only discards next.
* + undo_discard_redo_branches kills child and next.
*
* Basic invariants of the undo system:
* + root->parent == NULL
* + root->current is reachable from root via repeated child walk
* + saved is NULL or reachable the same way
* + pending is either NULL or a brand-new node not yet linked
* + when we commit, pending becomes current->child and current moves forward
* + when we undo, current = current->parent
* + when we type after undo, we free current->child (redo branch) first
*
* Or, visually,
*
* root ──> N1 ──> N2 ──> N3 ──> N4* ──> N5 ──> N6 (main timeline)
* ^ ^ ^
* | | |
* saved pending current
*
* + root : first edit ever, never has a parent
* + current : where we are right now in history
* + saved : points to the node that matches the on-disk file
* + pending : temporary node being built (committed → becomes current->child)
*
* If I do a double undo then type something:
*
* root ──> N1 ──> N2 ──> N3* ──> N4 ──> N5 (old N4→N5→N6 discarded)
* ^ ^
* | |
* current pending (new edit)
* |
* saved
*
* All four pointers point into the same tree → and should only be memory
* managed via the root node.
*/
undo_node_t *undo_node_new(undo_flag_t type);
void undo_node_free(undo_node_t *node);
void undo_node_free_branch(undo_node_t *node);
undo_tree_t *undo_tree_new(void);
void undo_tree_free(undo_tree_t *ut);
int undo_continue_pending(undo_flag_t type);
void undo_begin(undo_flag_t type);
void undo_append_char(char c);
void undo_append(const char *data, size_t len);
void undo_commit(void);
void undo_discard_redo_branches(struct undo_node *from);
undo_node_t *undo_parent_of(undo_node_t *node);
void undo_apply(const undo_node_t *node, int direction);
// void editor_undo(void);
// void editor_redo(void);
// void undo_mark_saved(void);
// int undo_depth(const undo_tree_t *t);
// int undo_can_undo(const undo_tree_t *t);
// int undo_can_redo(const undo_tree_t *t);
// void undo_tree_debug_dump(const undo_tree_t *t);
/* kill ring, marking, etc... */
void killring_flush(void);
@@ -281,8 +192,12 @@ int get_winsz(int *rows, int *cols);
void jump_to_position(int col, int 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(int at);
void row_append_row(struct erow *row, char *s, int len);
void row_insert_ch(struct erow *row, int at, int16_t c);
void row_delete_ch(struct erow *row, int at);
void insertch(int16_t c);
@@ -297,6 +212,7 @@ void editor_find_callback(char *query, int16_t c);
void editor_find(void);
char *editor_prompt(char*, void (*cb)(char*, int16_t));
void editor_openfile(void);
int first_nonwhitespace(struct erow *row);
void move_cursor_once(int16_t c, int interactive);
void move_cursor(int16_t c, int interactive);
void uarg_start(void);
@@ -452,13 +368,6 @@ init_editor(void)
editor.dirty = 0;
editor.mark_set = 0;
editor.mark_cury = editor.mark_curx = 0;
if (editor.undo != NULL) {
undo_tree_free(editor.undo);
editor.undo = NULL;
}
editor.undo = undo_tree_new();
}
@@ -478,10 +387,6 @@ reset_editor(void)
editor.filename = NULL;
}
if (editor.undo != NULL) {
undo_tree_free(editor.undo);
editor.undo = NULL;
}
init_editor();
}
@@ -527,6 +432,27 @@ ab_append(struct abuf *buf, const char *s, size_t len)
}
void
ab_prependch(struct abuf *buf, const char c)
{
ab_prepend(buf, &c, 1);
}
void
ab_prepend(struct abuf *buf, const char *s, const size_t len)
{
char *nc = realloc(buf->b, buf->len + len);
assert(nc != NULL);
memmove(nc + len, nc, buf->len);
memcpy(nc, s, len);
buf->b = nc;
buf->len += len;
}
void
ab_free(struct abuf *buf)
{
@@ -582,6 +508,12 @@ erow_render_to_cursor(struct erow *row, int cx)
continue;
}
if (b < 0x80) {
rx++;
j++;
continue;
}
size_t rem = (size_t)row->size - j;
size_t n = mbrtowc(&wc, &row->line[j], rem, &st);
@@ -635,6 +567,9 @@ erow_cursor_to_render(struct erow *row, int rx)
} else if (b < 0x20) {
w = 3; /* "\\xx" */
adv = 1;
} else if (b < 0x80) {
w = 1;
adv = 1;
} else {
size_t rem = (size_t)row->size - j;
size_t n = mbrtowc(&wc, &row->line[j], rem, &st);
@@ -676,6 +611,7 @@ erow_init(struct erow *row, int len)
row->render = NULL;
row->line = NULL;
row->cap = cap_growth(0, len) + 1; /* extra byte for NUL end */
row->dirty = 1;
row->line = malloc(row->cap);
assert(row->line != NULL);
@@ -775,366 +711,6 @@ erow_free(struct erow *row)
}
undo_node_t *
undo_node_new(undo_flag_t type)
{
undo_node_t *node = calloc1(sizeof(undo_node_t));
node->type = type;
node->row = node->col = 0;
node->next = NULL;
node->child = NULL;
ab_init(&node->text);
return node;
}
void
undo_node_free(undo_node_t *node)
{
if (node == NULL) {
return;
}
ab_free(&node->text);
undo_node_free(node->child);
undo_node_free(node->next);
free(node);
}
void
undo_node_free_branch(undo_node_t *node)
{
undo_node_t *next = NULL;
if (node == NULL) {
return;
}
while (node != NULL) {
next = node->next;
undo_node_free(node->child);
ab_free(&node->text);
free(node);
node = next;
}
}
undo_tree_t *
undo_tree_new(void)
{
undo_tree_t *tree = NULL;
tree = calloc1(sizeof(undo_tree_t));
tree->root = NULL;
tree->current = NULL;
tree->saved = NULL;
tree->pending = NULL;
return tree;
}
void
undo_tree_free(undo_tree_t *ut)
{
if (ut == NULL) {
return;
}
undo_node_free(ut->root);
undo_node_free(ut->pending);
if (debug_log == NULL) {
ut->root = NULL;
ut->current = NULL;
ut->saved = NULL;
ut->pending = NULL;
} else {
ut->root =
ut->current =
ut->saved =
ut->pending =
(void *)0xDEADBEEF;
}
free(ut);
}
int
undo_continue_pending(undo_flag_t type)
{
undo_tree_t *tree = editor.undo;
undo_node_t *node = tree->pending;
/* no pending node, so we need to start anew. */
if (node == NULL) {
return 0;
}
if (node->type != type) {
return 0;
}
if (node->row != editor.cury || node->col != editor.curx) {
return 0;
}
return 1;
}
/*
* undo_begin starts a new undo sequence. Note that it is a non-op
* if a new pending sequence doesn't need to be created.
*/
void
undo_begin(const undo_flag_t type)
{
undo_tree_t *tree = editor.undo;
undo_node_t *node = tree->pending;
if (undo_continue_pending(type)) {
return;
}
if (tree->pending != NULL) {
undo_commit();
}
node = undo_node_new(type);
assert(node != NULL);
node->type = type;
node->row = editor.cury;
node->col = editor.curx;
tree->pending = node;
}
void
undo_append_char(const char c)
{
undo_node_t *node = editor.undo->pending;
assert(node != NULL);
ab_appendch(&node->text, c);
}
void
undo_append(const char *data, const size_t len)
{
undo_node_t *node = editor.undo->pending;
assert(node != NULL);
ab_append(&node->text, data, len);
}
/* Finish the current batch and link it into the tree */
void
undo_commit(void)
{
undo_tree_t *tree = editor.undo;
undo_node_t *node = NULL;
if (tree->pending == NULL) {
return; /* nothing to commit */
}
node = tree->pending;
tree->pending = NULL;
if (tree->current && tree->current->child) {
undo_node_free_branch(tree->current->child);
tree->current->child = NULL;
}
if (tree->root == NULL) {
/* First edit ever */
tree->root = node;
tree->current = node;
} else if (tree->current == NULL) {
/* this shouldn't happen, so throw an
* assert to catch it.
*/
assert(tree->current != NULL);
tree->root = tree->current = node;
} else {
tree->current->child = node;
tree->current = node;
}
if (tree->saved && tree->current != tree->saved) {
tree->saved = NULL;
}
editor.dirty = 1;
}
void
undo_discard_redo_branches(struct undo_node *from)
{
undo_node_free(from->child);
from->child = NULL;
undo_node_free(from->next);
from->next = NULL;
}
undo_node_t *
undo_parent_of(undo_node_t *node)
{
undo_tree_t *tree = editor.undo;
undo_node_t *parent = tree->root;
if (tree->root == node) {
return NULL;
}
parent = tree->root;
while (parent != NULL && parent->child != node) {
parent = parent->child;
}
if (parent == NULL) {
return NULL;
}
return parent;
}
void
editor_undo(void)
{
undo_tree_t *tree = editor.undo;
undo_node_t *node = tree->current;
undo_node_t *parent = NULL;
if (node == NULL || node == tree->root) {
editor_set_status("Nothing to undo.");
return;
}
parent = undo_parent_of(node);
assert(parent != NULL);
undo_apply(node, -1);
tree->current = parent;
display_refresh();
}
void
editor_redo(void)
{
undo_tree_t *tree = editor.undo;
undo_node_t *node = tree->current;
if (node == NULL || node->child == NULL) {
editor_set_status("Nothing to redo.");
return;
}
tree->current = node->child;
undo_apply(node->child, 1);
display_refresh();
}
void
undo_apply(const struct undo_node *node, const int direction)
{
int row = node->row;
int col = node->col;
const char *data = node->text.b;
size_t len = node->text.len;
jump_to_position(col, row);
switch (node->type) {
case UNDO_PASTE:
case UNDO_INSERT:
if (direction > 0) {
for (size_t i = 0; i < len; i++) {
insertch((unsigned char)data[i]);
}
} else {
for (size_t i = 0; i < len; i++) {
deletech(KILLRING_NO_OP);
}
}
break;
case UNDO_DELETE:
if (direction > 0) {
for (size_t i = 0; i < len; i++) {
deletech(KILLRING_NO_OP);
}
} else {
for (size_t i = 0; i < len; i++) {
insertch((unsigned char)data[i]);
}
}
break;
case UNDO_NEWLINE:
if (direction > 0) {
newline();
} else {
if (editor.cury > 0) {
editor.curx = editor.row[editor.cury - 1].size;
row_append_row(&editor.row[editor.cury - 1],
editor.row[editor.cury].line,
editor.row[editor.cury].size);
delete_row(editor.cury);
}
}
break;
case UNDO_DELETE_ROW:
if (direction > 0) {
/* redo = delete the whole row again */
delete_row(editor.cury);
} else {
/* undo = re-insert the saved row (including final '\n') */
if (len > 0 && data[len - 1] == '\n') len--;
erow_insert(editor.cury, (char*)data, len);
/* cursor goes to start of re-inserted row */
editor.curx = 0;
}
break;
case UNDO_INDENT:
case UNDO_KILL_REGION:
/* These are more complex you can implement later */
/* For now just move cursor and say "not implemented yet" */
editor_set_status("Undo of %s not implemented yet",
direction > 0 ? "redo" : "complex op");
break;
default:
editor_set_status("Unknown undo type: %d", node->type);
break;
}
editor.dirty = 1;
}
void
killring_flush(void)
{
@@ -1191,7 +767,7 @@ killring_start_with_char(unsigned char ch)
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
row->dirty = 1;
}
@@ -1211,7 +787,7 @@ killring_append_char(unsigned char ch)
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
row->dirty = 1;
}
@@ -1229,7 +805,7 @@ killring_prepend_char(unsigned char ch)
memmove(&row->line[1], &row->line[0], row->size + 1);
row->line[0] = ch;
row->size++;
erow_update(row);
row->dirty = 1;
}
@@ -1437,7 +1013,7 @@ unindent_region(void)
if (del > 0) {
memmove(row->line, row->line + del, row->size - del + 1); /* +1 for NUL */
row->size -= del;
erow_update(row);
row->dirty = 1;
}
}
}
@@ -1596,6 +1172,13 @@ cursor_at_eol(void)
}
int
iswordchar(unsigned char c)
{
return isalnum(c) || c == '_' || strchr("/!@#$%^&*+-=~", c) != NULL;
}
void
find_next_word(void)
{
@@ -1603,7 +1186,7 @@ find_next_word(void)
move_cursor(ARROW_RIGHT, 1);
}
if (isalnum(editor.row[editor.cury].line[editor.curx])) {
if (iswordchar(editor.row[editor.cury].line[editor.curx])) {
while (!isspace(editor.row[editor.cury].line[editor.curx]) && !
cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
@@ -1630,7 +1213,7 @@ delete_next_word(void)
deletech(KILLRING_APPEND);
}
if (isalnum(editor.row[editor.cury].line[editor.curx])) {
if (iswordchar(editor.row[editor.cury].line[editor.curx])) {
while (!isspace(editor.row[editor.cury].line[editor.curx]) && !
cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
@@ -1767,7 +1350,7 @@ row_append_row(struct erow *row, char *s, int len)
memcpy(&row->line[row->size], s, len);
row->size += len;
row->line[row->size] = '\0';
erow_update(row);
row->dirty = 1;
editor.dirty++;
}
@@ -1789,7 +1372,7 @@ row_insert_ch(struct erow *row, int at, int16_t c)
row->size++;
row->line[at] = c & 0xff;
erow_update(row);
row->dirty = 1;
}
@@ -1802,7 +1385,7 @@ row_delete_ch(struct erow *row, int at)
memmove(&row->line[at], &row->line[at + 1], row->size - at);
row->size--;
erow_update(row);
row->dirty = 1;
editor.dirty++;
}
@@ -1834,8 +1417,8 @@ insertch(int16_t c)
void
deletech(uint8_t op)
{
struct erow *row = NULL;
unsigned char dch = 0;
struct erow *row = NULL;
unsigned char dch = 0;
if (editor.cury >= editor.nrows) {
return;
@@ -2261,7 +1844,7 @@ editor_find_callback(char* query, int16_t c)
row = &editor.row[current];
/* Skip rendering search on raw bytes — use line[] but respect render offsets */
erow_update(row);
row->dirty = 1;
char* search_start = row->render;
if (current == start_row && direction == 1 && last_match == -1) {
@@ -2355,6 +1938,14 @@ first_nonwhitespace(struct erow *row)
}
while (pos < row->size) {
if ((unsigned char)row->line[pos] < 0x80) {
if (!isspace((unsigned char)row->line[pos])) {
return pos;
}
pos++;
continue;
}
len = mbrtowc(&wc, &row->line[pos], row->size - pos, &state);
if (len == (size_t)-1 || len == (size_t)-2) {
break;
@@ -2378,8 +1969,8 @@ first_nonwhitespace(struct erow *row)
void
move_cursor_once(int16_t c, int interactive)
{
struct erow *row;
int reps;
struct erow *row;
int reps = 0;
row = (editor.cury >= editor.nrows) ? NULL : &editor.row[editor.cury];
@@ -2516,7 +2107,7 @@ newline(void)
row = &editor.row[editor.cury];
row->size = editor.curx;
row->line[row->size] = '\0';
erow_update(row);
row->dirty = 1;
editor.cury++;
editor.curx = 0;
}
@@ -2741,10 +2332,12 @@ process_kcommand(int16_t c)
case 'x':
exit(save_file());
case 'u':
editor_undo();
reps = uarg_get();
while (reps--);
editor_set_status("Undo not implemented.");
break;
case 'U':
editor_redo();
break;
case 'y':
reps = uarg_get();
@@ -3140,7 +2733,11 @@ draw_rows(struct abuf *ab)
}
} else {
row = &editor.row[filerow];
erow_update(row);
if (row->dirty) {
erow_update(row);
row->dirty = 0;
}
len = row->rsize - editor.coloffs;
if (len < 0) {
len = 0;
@@ -3244,11 +2841,15 @@ draw_message_line(struct abuf *ab)
void
scroll(void)
{
struct erow *row = NULL;
editor.rx = 0;
if (editor.cury < editor.nrows) {
editor.rx = erow_render_to_cursor(
&editor.row[editor.cury],
editor.curx);
row = &editor.row[editor.cury];
if (row->dirty == 1) {
erow_update(row);
}
editor.rx = erow_render_to_cursor(row, editor.curx);
}
if (editor.cury < editor.rowoffs) {
@@ -3278,6 +2879,7 @@ display_refresh(void)
scroll();
ab_append(&ab, ESCSEQ "?25l", 6);
ab_append(&ab, ESCSEQ "H", 3);
display_clear(&ab);
draw_rows(&ab);
@@ -3311,6 +2913,15 @@ editor_set_status(const char *fmt, ...)
}
int
kbhit(void)
{
int bytes_waiting;
ioctl(STDIN_FILENO, FIONREAD, &bytes_waiting);
return bytes_waiting > 0;
}
void
loop(void)
{
@@ -3327,7 +2938,9 @@ loop(void)
*
*/
if ((up = process_keypress()) != 0) {
while (process_keypress());
while (kbhit()) {
process_keypress();
}
}
}
}