47 Commits

Author SHA1 Message Date
a0d760c7d2 Checkpoint file split. 2025-11-28 02:38:54 -08:00
4db6077738 Splitting into separate files. 2025-11-28 00:29:52 -08:00
d9777c9f02 Remove erow. 2025-11-27 18:40:18 -08:00
d9c5a6696e Fix ESC+BKSP.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-27 14:04:55 -08:00
5c2571eba7 C-l should update window size.
Noticed this when splitting the screen in tmux.
2025-11-27 14:00:07 -08:00
9afd030b87 fix jump bug 2025-11-27 13:15:10 -08:00
2f198e611e less-compatible paging 2025-11-27 13:08:41 -08:00
e079726ced fix check for jump
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-27 13:02:18 -08:00
3c79e368fa Forgot to bump version in CMakeLists.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-27 12:51:53 -08:00
bd68b809cd various qol improvements
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
+ C-u works with C-f and C-b (and their delete counterparts).
+ Opening files now allows adding a line number after to jump to.
2025-11-27 12:48:10 -08:00
f200a7bfcd rename editor type. 2025-11-26 18:39:06 -08:00
58daeac6ad typedef struct abuf -> abuf 2025-11-26 18:38:32 -08:00
3ef6bab265 typedef struct erow -> erow 2025-11-26 18:35:51 -08:00
3800751bcf move var decl to top of function. 2025-11-26 17:57:59 -08:00
87272a669b remove debug log
just redirect stderr to a logfile like a normal person
2025-11-26 17:09:34 -08:00
b77748f7c0 add first post about ke to the readme 2025-11-26 16:47:58 -08:00
c70b502006 display message if KIONREAD ioctl fails. 2025-11-26 16:16:10 -08:00
e14d620887 remove comments, not needed 2025-11-26 16:14:25 -08:00
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
09881706f2 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-26 02:25:29 -08:00
2579bb01c8 fix C-e bug 2025-11-26 02:24:56 -08:00
a528b3fc3e universal args, more work on undo. 2025-11-26 02:12:37 -08:00
faa70af9b7 Working on undo system. 2025-11-26 01:45:56 -08:00
e1abaae6aa unindent, formatting fixups, and fix memleak
I think clion royally wrecked my formatting. Going to keep
fixing it up as I find stuff.
2025-11-26 00:26:07 -08:00
329b92f382 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-25 22:33:50 -08:00
12553f301b delete region works. 2025-11-25 22:31:03 -08:00
75b19042b9 add support for reloading the current file 2025-11-25 21:20:13 -08:00
1a184b1a08 minor fixups 2025-11-25 16:32:07 -08:00
acde895fb7 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-25 15:50:35 -08:00
78d522ba98 quality of life improvements 2025-11-25 15:50:05 -08:00
feb6667a24 add missing function declarations 2025-11-25 15:32:17 -08:00
Jeremy O'Brien
66c79e0762 fix manpage install with cmake 2025-11-25 13:51:28 -08:00
a51b98c31f switch nix build to cmake 2025-11-25 09:21:03 -08:00
707362574c fixups for nixos 2025-11-25 09:18:26 -08:00
56db8bd8d2 Indent region, version bump.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-25 00:47:06 -08:00
a32ef2ff53 move to first non-whitespace on next line
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-25 00:20:32 -08:00
0c0c3d9ce5 fix arrows in search 2025-11-25 00:12:44 -08:00
7245003769 cleanups 2025-11-24 23:05:40 -08:00
20 changed files with 3194 additions and 1153 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.3.3")
set(KE_VERSION "1.5.6")
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")
@@ -20,7 +20,15 @@ endif()
include(GNUInstallDirs)
# Add executable
add_executable(ke main.c)
add_executable(ke
abuf.c
term.c
buffer.c
editor.c
core.c
core.h
main.c
)
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
install(TARGETS ke RUNTIME DESTINATION bin)
install(FILES ke.1 TYPE MAN)

View File

@@ -11,8 +11,11 @@ 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 editor.c core.c
HDRS := abuf.h term.h buffer.h editor.h core.h
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS)
.PHONY: install
#install: $(TARGET)
@@ -26,3 +29,12 @@ clean:
.PHONY: test.txt
test.txt:
cp test.txt.bak $@
.PHONY: gdb
gdb:
@test -f $(TARGET).pid || (echo "error: $(TARGET).pid not found" >&2; exit 1)
@gdb -p $$(cat $(TARGET).pid | tr -d ' \t\n\r') ./$(TARGET)
.PHONY: cloc
cloc:
cloc $(SRCS) $(HDRS)

View File

@@ -16,3 +16,9 @@ To get verbose ASAN messages:
export LSAN_OPTIONS=verbosity=1:log_threads=1
Released under an ISC license.
Started by following along with kilo:
https://viewsourcecode.org/snaptoken/kilo/
E.g., in the devlogs
https://log.wntrmute.dev/2020/02/20200207

17
TODO
View File

@@ -1,15 +1,18 @@
[X] goto-line
[X] text-corruption bug
[ ] alt-modifiers
[ ] refresh-screen
[ ] functions -> keymapping?
[x] refresh-screen
[ ] functions -> keymapping? (what did this even mean)
[X] rendering: need to skip over control characters like we do with tabs
[X] control-d -> delete
[ ] control-g -> exit buf prompt
[ ] load-file prompt on dirty buffer
[x] control-g -> exit buf prompt
[x] load-file prompt on dirty buffer
[ ] multiple files
[x] utf-8
[x] kill ring
[x] mark/region
What would it take for ke to be your daily drives?
+ C-u
+ Alt nav (backspace, delete, f, b, etc)
[x] C-u (repeat actions)
[x] Alt nav (backspace, delete, f, b, etc)
[-] undo tree (C-k u/U)

97
abuf.c Normal file
View File

@@ -0,0 +1,97 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "abuf.h"
#include "core.h"
void
ab_init(abuf *buf)
{
assert(buf != NULL);
buf->b = NULL;
buf->size = buf->cap = 0;
}
void
ab_init_cap(abuf *buf, const size_t cap)
{
buf->b = calloc(cap, 1);
buf->size = 0;
buf->cap = 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;
}
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;
}

30
abuf.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* abuf.h - append/prepend buffer utilities
*/
#ifndef ABUF_H
#define ABUF_H
#include <stddef.h>
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_init_cap(abuf *buf, size_t cap);
void ab_resize(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, const char c);
void ab_prepend(abuf *buf, const char *s, const size_t len);
void ab_free(abuf *buf);
#endif

503
buffer.c Normal file
View File

@@ -0,0 +1,503 @@
/* buffer.c - multiple file buffers */
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "abuf.h"
#include "buffer.h"
#include "core.h"
#include "editor.h"
#define NO_NAME "[No Name]"
/* externs from other modules */
char *editor_prompt(const 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 != TAB_KEY) {
return;
}
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(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 *
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);
}
buffer *
buffer_current(void)
{
if (editor.curbuf < 0 || editor.curbuf >= editor.bufcount) {
return NULL;
}
return editor.buffers[editor.curbuf];
}
int
buffer_is_unnamed_and_empty(const buffer *b)
{
if (b == NULL) {
return 0;
}
if (b->filename != NULL) {
return 0;
}
if (b->dirty) {
return 0;
}
if (b->nrows != 0) {
return 0;
}
if (b->row != NULL) {
return 0;
}
return 1;
}
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;
if (editor.bufcount <= 1) {
editor_set_status("No other buffers.");
return;
}
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);
}

50
buffer.h Normal file
View File

@@ -0,0 +1,50 @@
#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;
/* Access current buffer and convenient aliases for file-specific fields */
buffer *buffer_current(void);
#define CURBUF (buffer_current())
#define EROW (CURBUF->row)
#define ENROWS (CURBUF->nrows)
#define ECURX (CURBUF->curx)
#define ECURY (CURBUF->cury)
#define ERX (CURBUF->rx)
#define EROWOFFS (CURBUF->rowoffs)
#define ECOLOFFS (CURBUF->coloffs)
#define EFILENAME (CURBUF->filename)
#define EDIRTY (CURBUF->dirty)
#define EMARK_SET (CURBUF->mark_set)
#define EMARK_CURX (CURBUF->mark_curx)
#define EMARK_CURY (CURBUF->mark_cury)
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);
/* Helpers */
int buffer_is_unnamed_and_empty(const buffer *b);
#endif

124
core.c Normal file
View File

@@ -0,0 +1,124 @@
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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
char
nibble_to_hex(char c)
{
c &= 0xf;
if (c < 10) {
return (char)('0' + c);
}
return (char)('A' + (c - 10));
}
void
swap_int(int *first, int *second)
{
*first ^= *second;
*second ^= *first;
*first ^= *second;
}
int
next_power_of_2(int n)
{
if (n < 2) {
n = 2;
}
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}
int
cap_growth(int cap, int sz)
{
if (cap == 0) {
cap = INITIAL_CAPACITY;
}
while (cap <= sz) {
cap = next_power_of_2(cap + 1);
}
return cap;
}
size_t
kstrnlen(const char *buf, const size_t max)
{
if (buf == NULL) {
return 0;
}
return strnlen(buf, max);
}
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);
}

41
core.h Normal file
View File

@@ -0,0 +1,41 @@
#ifndef KE_CORE_H
#define KE_CORE_H
#include <stddef.h>
#define calloc1(sz) calloc(1, sz)
#define INITIAL_CAPACITY 8
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
char nibble_to_hex(char c);
void swap_int(int *first, int *second);
int next_power_of_2(int n);
int cap_growth(int cap, int sz);
size_t kstrnlen(const char *buf, size_t max);
void kwrite(int fd, const char *buf, int len);
void die(const char *s);
#endif

View File

@@ -1,16 +1,31 @@
{
lib,
installShellFiles,
stdenv,
cmake,
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KE_VERSION \".+\"\\).*" l != null) (throw "KE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "ke";
version = "1.3.3";
inherit version;
src = lib.cleanSource ./.;
nativeBuildInputs = [ installShellFiles ];
nativeBuildInputs = [
cmake
installShellFiles
];
cmakeFlags = [
"-DENABLE_ASAN=on"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
@@ -18,10 +33,8 @@ stdenv.mkDerivation {
mkdir -p $out/bin
cp ke $out/bin/
installManPage ../ke.1
runHook postInstall
'';
postInstall = ''
installManPage ke.1
'';
}

124
editor.c Normal file
View File

@@ -0,0 +1,124 @@
/* editor.c - editor-wide state and functions */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "abuf.h"
#include "buffer.h"
#include "core.h"
#include "editor.h"
#include "term.h"
/*
* Global editor instance
*/
struct editor editor = {
.cols = 0,
.rows = 0,
.curx = 0,
.cury = 0,
.rx = 0,
.mode = 0,
.nrows = 0,
.rowoffs = 0,
.coloffs = 0,
.row = NULL,
.killring = NULL,
.kill = 0,
.no_kill = 0,
.filename = NULL,
.dirty = 0,
.dirtyex = 0,
.mark_set = 0,
.mark_curx = 0,
.mark_cury = 0,
.uarg = 0,
.ucount = 0,
.msgtm = 0,
.buffers = NULL,
.bufcount = 0,
.curbuf = -1,
};
void
editor_set_status(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(editor.msg, sizeof(editor.msg), fmt, ap);
va_end(ap);
editor.msgtm = time(NULL);
}
void
init_editor(void)
{
editor.cols = 0;
editor.rows = 0;
if (get_winsz(&editor.rows, &editor.cols) == -1) {
die("can't get window size");
}
editor.rows--; /* status bar */
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:
* killing / yanking across files is helpful, and killring
* is initialized to NULL at program start.
*/
editor.kill = 0;
editor.no_kill = 0;
editor.msg[0] = '\0';
editor.msgtm = 0;
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();
}
}
void
reset_editor(void)
{
/* Clear current working set. Notably, 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;
}
editor.dirty = 0;
editor.mark_set = 0;
editor.mark_cury = editor.mark_curx = 0;
}

47
editor.h Normal file
View File

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

35
ke.1
View File

@@ -22,11 +22,18 @@ grandeur. Many commands work with and without control; for example,
saving a file can be done with either C-k s or C-k C-s. Other commands work
with ESC or CTRL.
.Sh K-COMMANDS
k-command mode can be exited with ESC or C-g.
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-k BACKSPACE
Delete from the cursor to the beginning of the line.
.It C-k SPACE
Toggle the mark.
.It C-k -
If the mark is set, unindent the region.
.It C-k =
If the mark is set, indent the region.
.It C-k c
Clear (flush) the kill ring.
.It C-k d
Delete from the cursor to the end of the line.
.It C-k C-d
@@ -36,32 +43,48 @@ Edit a new file. Also C-k C-e.
.It C-k f
Incremental find.
.It C-k g
Go to a specific line. Also C-k C-g.
Go to a specific line.
.It C-k j
Jump to the mark.
.It C-k l
List the number of lines of code in a saved file.
.It C-k m
Run make(1).
.It C-k q
exit the editor. Also C-k C-q.
Exit the editor. If the file has unsaved changes,
a warning will be printed; a second C-k q will exit.
.It C-k C-q
Immediately exit the editor.
.It C-k C-r
Reload the current buffer from disk.
.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
Undo changes.
.It C-k U
Redo changes.
.It C-k x
save the file and exit. Also C-k C-x.
.It C-k y
Yank the killring.
Yank the kill ring.
.It C-k \[char92]
Dump core.
.El
.Sh OTHER KEYBINDINGS
.Bl -tag -width xxxxxxxxxxxx -offset indent
.It C-g
In general, C-g cancels an operation.
.It C-l
Refresh the display.
.It C-s
Incremental find.
.It C-u
Universal argument. C-u followed by numbers will repeat an
operation n times.
.It C-w
Kill the region if the mark is set.
.It C-y
Yank the killring.
Yank the kill ring.
.It ESC BACKSPACE
Delete the previous word.
.It ESC b
@@ -71,7 +94,7 @@ Delete the next word.
.It ESC f
Move to the next word.
.It ESC w
Save the region (if the mark is set) to the killring.
Save the region (if the mark is set) to the kill ring.
.It
.El
.Sh FIND

2800
main.c

File diff suppressed because it is too large Load Diff

162
scratch.c Normal file
View File

@@ -0,0 +1,162 @@
/*
* scratch.c - ideas in progress
*/
#define REFLOW_MARGIN 72
void
reflow_region(void)
{
int start_row, end_row, i, col, wlen, this_len;
struct erow *row;
struct abuf buf = ABUF_INIT;
struct abuf out = ABUF_INIT;
int in_paragraph = 0;
int indent_len = 0;
char indent[REFLOW_MARGIN + 1];
char word[REFLOW_MARGIN + 1];
char *e = NULL;
char *p = NULL;
char *s = NULL;
if (editor.mark_set) {
if (editor.mark_cury < editor.cury ||
(editor.mark_cury == editor.cury &&
editor.mark_curx < editor.curx)) {
start_row = editor.mark_cury;
end_row = editor.cury;
} else {
start_row = editor.cury;
end_row = editor.mark_cury;
}
} else {
start_row = end_row = editor.cury;
while (start_row > 0 && editor.row[start_row - 1].size > 0) {
start_row--;
}
while (end_row < editor.nrows - 1 &&
editor.row[end_row + 1].size > 0) {
end_row++;
}
}
if (start_row >= editor.nrows) {
return;
}
if (end_row >= editor.nrows) {
end_row = editor.nrows - 1;
}
for (i = start_row; i <= end_row; i++) {
row = &editor.row[i];
if (row->size == 0) {
if (in_paragraph) {
ab_append(&buf, "\n", 1);
in_paragraph = 0;
}
ab_append(&buf, "\n", 1);
continue;
}
if (!in_paragraph) {
indent_len = 0;
while (indent_len < row->size &&
(row->line[indent_len] == ' ' ||
row->line[indent_len] == '\t')) {
indent[indent_len] = row->line[indent_len], indent_len++;
}
indent[indent_len] = '\0';
in_paragraph = 1;
}
ab_append(&buf, row->line + indent_len, row->size - indent_len);
ab_append(&buf, " ", 1);
}
if (in_paragraph) {
ab_append(&buf, "\n", 1);
}
p = buf.b;
col = 0;
while (p != NULL && *p != '\0') {
while (*p && isspace((unsigned char)*p) &&
*p != '\n') {
p++;
}
if (*p == '\0') {
break;
}
wlen = 0;
while (*p && !isspace((unsigned char)*p)) {
if (wlen < REFLOW_MARGIN)
word[wlen++] = *p;
p++;
}
word[wlen] = '\0';
if (*p == '\n' && (p[1] == '\n' || p[1] == '\0')) {
ab_append(&out, "\n", 1); /* flush */
col = 0;
p++; /* consume the extra \n */
continue;
}
this_len = wlen;
if (col > 0) {
this_len++; /* space before word */
}
if (col == 0) {
ab_append(&out, indent, indent_len);
col = indent_len;
}
if (col + this_len > REFLOW_MARGIN && col > 0) {
ab_append(&out, "\n", 1);
ab_append(&out, indent, indent_len);
col = indent_len;
}
if (col > 0) {
ab_append(&out, " ", 1);
col++;
}
ab_append(&out, word, wlen);
col += wlen;
}
if (col > 0) {
ab_append(&out, "\n", 1);
}
/* the old switcharoo */
buf = out;
ab_free(&out);
for (i = end_row; i >= start_row; i--) {
delete_row(i);
}
s = buf.b;
while ((e = strchr(s, '\n'))) {
erow_insert(start_row++, s, e - s);
s = e + 1;
}
ab_free(&buf);
editor.dirty++;
editor_set_status("Region reflowed to %d columns", REFLOW_MARGIN);
}

86
term.c Normal file
View File

@@ -0,0 +1,86 @@
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#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;
}

22
term.h Normal file
View File

@@ -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 */

104
undo.c Normal file
View File

@@ -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);

44
undo.h Normal file
View File

@@ -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