2 Commits

Author SHA1 Message Date
881e2b3393 Continue C++ rewrite. 2025-11-25 09:17:50 -08:00
bbb23916a0 Start C++ rewrite. 2025-11-24 23:36:19 -08:00
42 changed files with 4368 additions and 4691 deletions

View File

@@ -1,56 +1,74 @@
# CMake configuration for Kyle's Editor (ke)
cmake_minimum_required(VERSION 3.15)
project(ke C) # Specify C language explicitly
project(ke VERSION 1.3.3 LANGUAGES CXX)
set(CMAKE_C_STANDARD 99)
set(KE_VERSION "2.1.1")
# Project metadata
set(KE_VERSION "1.3.3")
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")
# C++17 standard requirement
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Optionally enable AddressSanitizer (ASan)
# Source files
set(KE_SOURCES
main.cc
abuf.cc
erow.cc
terminal.cc
input_handler.cc
display.cc
file_io.cc
killring.cc
)
# Header files
set(KE_HEADERS
ke_constants.h
abuf.h
erow.h
terminal.h
input_handler.h
display.h
file_io.h
killring.h
)
# Build options
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
if (ENABLE_ASAN)
# Create executable target
add_executable(ke ${KE_SOURCES})
# Target compile features
target_compile_features(ke PRIVATE cxx_std_17)
# Target compile options
target_compile_options(ke PRIVATE
-Wall
-Wextra
-pedantic
-Wshadow
-Werror
-g
)
# Target compile definitions
target_compile_definitions(ke PRIVATE
KE_VERSION="ke version ${KE_VERSION}"
_DEFAULT_SOURCE
_XOPEN_SOURCE
)
# AddressSanitizer support
if(ENABLE_ASAN)
message(STATUS "ASan enabled")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
# Ensure the sanitizer is linked too (especially important on some platforms)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
target_compile_options(ke PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(ke PRIVATE -fsanitize=address)
endif()
# Installation rules
include(GNUInstallDirs)
set(SOURCES
abuf.c
core.c
term.c
buffer.c
editor.c
editing.c
killring.c
process.c
undo.c
main.c
)
set(HEADERS
abuf.h
core.h
term.h
buffer.h
editor.h
editing.h
killring.h
process.h
undo.h
)
# Add executable
add_executable(ke ${SOURCES} ${HEADERS})
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
install(TARGETS ke RUNTIME DESTINATION bin)
install(FILES ke.1 TYPE MAN)
install(TARGETS ke RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ke.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)

View File

@@ -2,22 +2,53 @@ TARGET := ke
KE_VERSION := devel
DEST := $(HOME)/.local/bin/$(TARGET)
CC := gcc
CXX := g++
CFLAGS := -Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g
CFLAGS += -Wno-unused-result
CFLAGS += -D_DEFAULT_SOURCE -D_XOPEN_SOURCE
CFLAGS += -fsanitize=address -fno-omit-frame-pointer
CXXFLAGS := -Wall -Wextra -pedantic -Wshadow -Werror -std=c++17 -g
CXXFLAGS += -Wno-unused-result
CXXFLAGS += -D_DEFAULT_SOURCE -D_XOPEN_SOURCE
CXXFLAGS += -DKE_VERSION='"ke $(KE_VERSION)"'
CXXFLAGS += -fsanitize=address -fno-omit-frame-pointer
LDFLAGS := -fsanitize=address
SOURCES := main.cc abuf.cc erow.cc terminal.cc input_handler.cc display.cc file_io.cc killring.cc
OBJECTS := main.o abuf.o erow.o terminal.o input_handler.o display.o file_io.o killring.o
all: $(TARGET) test.txt
SRCS := main.c abuf.c core.c term.c buffer.c editor.c editing.c killring.c \
process.c undo.c
HDRS := abuf.h core.h term.h buffer.h editor.h editing.c killring.h \
process.h undo.h
$(TARGET): $(OBJECTS)
$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJECTS) $(LDFLAGS)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LDFLAGS)
main.o: main.cc ke_constants.h
$(CXX) $(CXXFLAGS) -c main.cc
abuf.o: abuf.cc abuf.h
$(CXX) $(CXXFLAGS) -c abuf.cc
erow.o: erow.cc erow.h
$(CXX) $(CXXFLAGS) -c erow.cc
terminal.o: terminal.cc terminal.h
$(CXX) $(CXXFLAGS) -c terminal.cc
input_handler.o: input_handler.cc input_handler.h ke_constants.h
$(CXX) $(CXXFLAGS) -c input_handler.cc
display.o: display.cc display.h ke_constants.h
$(CXX) $(CXXFLAGS) -c display.cc
file_io.o: file_io.cc file_io.h
$(CXX) $(CXXFLAGS) -c file_io.cc
killring.o: killring.cc killring.h
$(CXX) $(CXXFLAGS) -c killring.cc
.PHONY: install
#install: $(TARGET)
@@ -26,17 +57,9 @@ install:
clean:
rm -f $(TARGET)
rm -f $(OBJECTS)
rm -f asan.log*
.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,9 +16,3 @@ 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,18 +1,15 @@
[X] goto-line
[X] text-corruption bug
[ ] alt-modifiers
[x] refresh-screen
[ ] functions -> keymapping? (what did this even mean)
[ ] refresh-screen
[ ] functions -> keymapping?
[X] rendering: need to skip over control characters like we do with tabs
[X] control-d -> delete
[x] control-g -> exit buf prompt
[x] load-file prompt on dirty buffer
[ ] control-g -> exit buf prompt
[ ] 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?
[x] C-u (repeat actions)
[x] Alt nav (backspace, delete, f, b, etc)
[-] undo tree (C-k u/U)
+ C-u
+ Alt nav (backspace, delete, f, b, etc)

131
abuf.c
View File

@@ -1,131 +0,0 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "abuf.h"
#include "core.h"
static void
abuf_grow(abuf *buf, size_t delta)
{
if (buf->cap - buf->size < delta) {
ab_resize(buf, buf->cap + delta);
}
}
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)
{
ab_init(buf);
if (cap > 0) {
ab_resize(buf, cap);
}
}
void
ab_resize(abuf *buf, size_t cap)
{
char *newbuf = NULL;
cap = cap_growth(buf->cap, cap) + 1;
newbuf = realloc(buf->b, cap);
assert(newbuf != NULL);
buf->cap = cap;
buf->b = newbuf;
}
void
ab_appendch(abuf *buf, char c)
{
abuf_grow(buf, 1);
ab_append(buf, &c, 1);
}
void
ab_append(abuf *buf, const char *s, size_t len)
{
char *nc = NULL;
abuf_grow(buf, len);
nc = buf->b;
memcpy(&nc[buf->size], s, len);
buf->b = nc;
buf->size += len;
}
void
ab_append_ab(abuf *buf, const abuf *other)
{
assert(buf != NULL);
if (other == NULL) {
return;
}
ab_append(buf, other->b, other->size);
}
void
ab_prependch(abuf *buf, const char c)
{
abuf_grow(buf, 1);
ab_prepend(buf, &c, 1);
}
void
ab_prepend(abuf *buf, const char *s, const size_t len)
{
char *nc = NULL;
abuf_grow(buf, len);
nc = buf->b;
assert(nc != NULL);
memmove(nc + len, nc, buf->size);
memcpy(nc, s, len);
buf->b = nc;
buf->size += len;
}
void
ab_prepend_ab(abuf *buf, const abuf *other)
{
assert(buf != NULL);
if (other == NULL) {
return;
}
ab_prepend(buf, other->b, other->size);
}
void
ab_free(abuf *buf)
{
free(buf->b);
buf->b = NULL;
buf->size = 0;
buf->cap = 0;
}

17
abuf.cc Normal file
View File

@@ -0,0 +1,17 @@
#include "abuf.h"
namespace ke {
void abuf::append(std::string_view s) {
buffer_.append(s);
}
void abuf::append(const char* s, std::size_t len) {
buffer_.append(s, len);
}
void abuf::append(char c) {
buffer_.push_back(c);
}
} // namespace ke

69
abuf.h
View File

@@ -1,32 +1,57 @@
/*
* abuf.h - append/prepend buffer utilities
#ifndef ABUF_HPP
#define ABUF_HPP
#include <string>
#include <string_view>
#include <cstddef>
namespace ke {
/**
* Append buffer class for efficient string building.
* C++17 implementation with RAII memory management.
*/
#ifndef KE_ABUF_H
#define KE_ABUF_H
class abuf {
public:
// Constructors
abuf() noexcept = default;
#include <stddef.h>
// Deleted copy constructor and assignment (use move semantics)
abuf(const abuf&) = delete;
abuf& operator=(const abuf&) = delete;
// Move constructor and assignment
abuf(abuf&&) noexcept = default;
abuf& operator=(abuf&&) noexcept = default;
typedef struct abuf {
char *b;
size_t size;
size_t cap;
} abuf;
// Destructor (automatic cleanup via std::string)
~abuf() = default;
// Append methods
void append(std::string_view s);
void append(const char* s, std::size_t len);
void append(char c);
#define ABUF_INIT {NULL, 0, 0}
// Accessors
[[nodiscard]] const char* data() const noexcept { return buffer_.data(); }
[[nodiscard]] std::size_t size() const noexcept { return buffer_.size(); }
[[nodiscard]] std::size_t length() const noexcept { return buffer_.length(); }
[[nodiscard]] bool empty() const noexcept { return buffer_.empty(); }
[[nodiscard]] std::size_t capacity() const noexcept { return buffer_.capacity(); }
// Get the underlying string
[[nodiscard]] const std::string& str() const noexcept { return buffer_; }
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_append_ab(abuf *buf, const abuf *other);
void ab_prependch(abuf *buf, const char c);
void ab_prepend(abuf *buf, const char *s, const size_t len);
void ab_prepend_ab(abuf *buf, const abuf *other);
void ab_free(abuf *buf);
// Clear the buffer
void clear() noexcept { buffer_.clear(); }
// Reserve capacity
void reserve(std::size_t new_cap) { buffer_.reserve(new_cap); }
#endif
private:
std::string buffer_;
};
} // namespace ke
#endif // ABUF_HPP

463
buffer.c
View File

@@ -1,463 +0,0 @@
/* 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;
size_t i = 0;
if (name == NULL) {
return -1;
}
for (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 i = 0;
size_t plen = (prefix ? strlen(prefix) : 0);
for (i = 0; i < editor.bufcount; i++) {
matched = 0;
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);
}
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 = 0;
editor.bufcap = 0;
idx = buffer_add_empty();
buffer_switch(idx);
}
static void
buffer_list_resize(void)
{
buffer **newlist = NULL;
if (editor.bufcount == editor.bufcap) {
editor.bufcap = (size_t)cap_growth((int)editor.bufcap,
(int)editor.bufcount + 1);
newlist = realloc(editor.buffers, sizeof(buffer *) * editor.bufcap);
assert(newlist != NULL);
editor.buffers = newlist;
}
}
int
buffer_add_empty(void)
{
buffer *buf = NULL;
int idx = 0;
buffer_list_resize();
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;
undo_tree_init(&buf->undo);
editor.buffers[editor.bufcount] = buf;
idx = (int)editor.bufcount;
editor.bufcount++;
return idx;
}
buffer *
buffer_current(void)
{
if (editor.bufcount == 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 || (size_t)idx >= editor.bufcount) {
return;
}
if (editor.curbuf == (size_t)idx) {
return;
}
b = editor.buffers[idx];
editor.curbuf = (size_t)idx;
editor.dirtyex = 1;
editor_set_status("Switched to buffer %d: %s", editor.curbuf, buffer_name(b));
}
void
buffer_next(void)
{
size_t idx = 0;
if (editor.bufcount <= 1) {
return;
}
idx = (editor.curbuf + 1) % editor.bufcount;
buffer_switch((int)idx);
}
void
buffer_prev(void)
{
size_t idx = 0;
if (editor.bufcount <= 1) {
return;
}
idx = (editor.curbuf == 0) ? (editor.bufcount - 1) : (editor.curbuf - 1);
buffer_switch((int)idx);
}
void
buffer_close_current(void)
{
buffer *b = NULL;
size_t closing = 0;
int target = 0;
int nb = 0;
/* sanity check */
if (editor.bufcount == 0 || editor.curbuf >= editor.bufcount) {
editor_set_status("No buffer to close.");
return;
}
closing = editor.curbuf;
if (editor.bufcount > 1) {
target = (closing > 0) ? (int) (closing - 1) : (int) (closing + 1);
buffer_switch(target);
} else {
nb = buffer_add_empty();
buffer_switch(nb);
}
b = editor.buffers[closing];
if (b) {
if (b->row) {
for (size_t 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 = 0;
} 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 open buffer: %s", name);
}
free(name);
}

View File

@@ -1,50 +0,0 @@
#ifndef KE_BUFFER_H
#define KE_BUFFER_H
#include "abuf.h"
#include "undo.h"
typedef struct buffer {
size_t curx, cury;
size_t rx;
size_t nrows;
size_t rowoffs, coloffs;
abuf *row;
char *filename;
int dirty;
int mark_set;
size_t mark_curx, mark_cury;
undo_tree undo;
} buffer;
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);
int buffer_is_unnamed_and_empty(const buffer *b);
#endif

151
core.c
View File

@@ -1,151 +0,0 @@
#include <sys/stat.h>
#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
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
swap_size_t(size_t *first, size_t *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);
}

52
core.h
View File

@@ -1,52 +0,0 @@
#ifndef KE_CORE_H
#define KE_CORE_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
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
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);
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,31 +1,16 @@
{
lib,
stdenv,
cmake,
installShellFiles,
stdenv,
...
}:
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";
inherit version;
version = "1.3.3";
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
installShellFiles
];
cmakeFlags = [
"-DENABLE_ASAN=on"
"-DCMAKE_BUILD_TYPE=Debug"
];
nativeBuildInputs = [ installShellFiles ];
installPhase = ''
runHook preInstall
@@ -33,8 +18,10 @@ stdenv.mkDerivation {
mkdir -p $out/bin
cp ke $out/bin/
installManPage ../ke.1
runHook postInstall
'';
postInstall = ''
installManPage ke.1
'';
}

237
display.cc Normal file
View File

@@ -0,0 +1,237 @@
#include "display.h"
#include "ke_constants.h"
#include "abuf.h"
#include "erow.h"
#include <termios.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cassert>
// Need access to main.cc structures and functions
extern void erow_update(struct erow *row);
extern int erow_render_to_cursor(struct erow *row, int cx);
// erow definition (needed for accessing row fields)
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition (needed for display operations)
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void Display::clear(ke::abuf* ab) {
if (ab == nullptr) {
(void)write(STDOUT_FILENO, ESCSEQ "2J", 4);
(void)write(STDOUT_FILENO, ESCSEQ "H", 3);
} else {
ab->append(ESCSEQ "2J", 4);
ab->append(ESCSEQ "H", 3);
}
}
void Display::draw_rows(editor_t* editor, ke::abuf* ab) {
assert(editor->cols >= 0);
char* buf = new char[editor->cols];
int buflen, filerow, padding;
int y;
for (y = 0; y < editor->rows; y++) {
filerow = y + editor->rowoffs;
if (filerow >= editor->nrows) {
if ((editor->nrows == 0) && (y == editor->rows / 3)) {
buflen = snprintf(buf,
editor->cols,
"%s",
KE_VERSION);
padding = (editor->rows - buflen) / 2;
if (padding) {
ab->append("|", 1);
padding--;
}
while (padding--)
ab->append(" ", 1);
ab->append(buf, buflen);
} else {
ab->append("|", 1);
}
} else {
erow_update(&editor->row[filerow]);
buflen = editor->row[filerow].rsize - editor->coloffs;
if (buflen < 0) {
buflen = 0;
}
if (buflen > editor->cols) {
buflen = editor->cols;
}
ab->append(editor->row[filerow].render + editor->coloffs,
buflen);
}
ab->append(ESCSEQ "K", 3);
ab->append("\r\n", 2);
}
delete[] buf;
}
char Display::status_mode_char(int mode) {
switch (mode) {
case MODE_NORMAL:
return 'N';
case MODE_KCOMMAND:
return 'K';
case MODE_ESCAPE:
return 'E';
default:
return '?';
}
}
void Display::draw_status_bar(editor_t* editor, ke::abuf* ab) {
char* status = new char[editor->cols];
char* rstatus = new char[editor->cols];
char* mstatus = new char[editor->cols];
int len, rlen;
len = snprintf(status,
editor->cols,
"%c%cke: %.20s - %d lines",
status_mode_char(editor->mode),
editor->dirty ? '!' : '-',
editor->filename ? editor->filename : "[no file]",
editor->nrows);
if (editor->mark_set) {
snprintf(mstatus,
editor->cols,
" | M: %d, %d ",
editor->mark_curx + 1,
editor->mark_cury + 1);
} else {
snprintf(mstatus, editor->cols, " | M:clear ");
}
rlen = snprintf(rstatus,
editor->cols,
"L%d/%d C%d %s",
editor->cury + 1,
editor->nrows,
editor->curx + 1,
mstatus);
if (len > editor->cols) {
len = editor->cols;
}
ab->append(ESCSEQ "7m", 4);
ab->append(status, len);
while (len < editor->cols) {
if (editor->cols - len == rlen) {
ab->append(rstatus, rlen);
break;
} else {
ab->append(" ", 1);
len++;
}
len++;
}
ab->append(ESCSEQ "m", 3);
ab->append("\r\n", 2);
delete[] status;
delete[] rstatus;
delete[] mstatus;
}
void Display::draw_message_line(editor_t* editor, ke::abuf* ab) {
int len = strlen(editor->msg);
ab->append(ESCSEQ "K", 3);
if (len > editor->cols) {
len = editor->cols;
}
if (len && ((time(nullptr) - editor->msgtm) < MSG_TIMEO)) {
ab->append(editor->msg, len);
}
}
void Display::scroll(editor_t* editor) {
editor->rx = 0;
if (editor->cury < editor->nrows) {
editor->rx = erow_render_to_cursor(
&editor->row[editor->cury],
editor->curx);
}
if (editor->cury < editor->rowoffs) {
editor->rowoffs = editor->cury;
}
if (editor->cury >= editor->rowoffs + editor->rows) {
editor->rowoffs = editor->cury - editor->rows + 1;
}
if (editor->rx < editor->coloffs) {
editor->coloffs = editor->rx;
}
if (editor->rx >= editor->coloffs + editor->cols) {
editor->coloffs = editor->rx - editor->cols + 1;
}
}
void Display::refresh(editor_t* editor) {
char buf[32];
ke::abuf ab;
scroll(editor);
ab.append(ESCSEQ "?25l", 6);
clear(&ab);
draw_rows(editor, &ab);
draw_status_bar(editor, &ab);
draw_message_line(editor, &ab);
snprintf(buf,
sizeof(buf),
ESCSEQ "%d;%dH",
(editor->cury - editor->rowoffs) + 1,
(editor->rx - editor->coloffs) + 1);
ab.append(buf, strnlen(buf, 32));
ab.append(ESCSEQ "?25h", 6);
(void)write(STDOUT_FILENO, ab.data(), ab.size());
}
} // namespace ke

42
display.h Normal file
View File

@@ -0,0 +1,42 @@
#ifndef DISPLAY_HPP
#define DISPLAY_HPP
#include <cstddef>
// Forward declarations
struct editor_t;
namespace ke {
class abuf; // Forward declaration of ke::abuf
/**
* Display class for screen rendering and refresh operations.
*/
class Display {
public:
Display() = default;
~Display() = default;
// Deleted copy constructor and assignment
Display(const Display&) = delete;
Display& operator=(const Display&) = delete;
// Main display operations
static void refresh(editor_t* editor);
static void clear(ke::abuf* ab);
// Drawing operations
static void draw_rows(editor_t* editor, ke::abuf* ab);
static void draw_status_bar(editor_t* editor, ke::abuf* ab);
static void draw_message_line(editor_t* editor, ke::abuf* ab);
// Scrolling
static void scroll(editor_t* editor);
private:
static char status_mode_char(int mode);
};
} // namespace ke
#endif // DISPLAY_HPP

View File

@@ -0,0 +1,115 @@
# Constants Refactoring Documentation
## Overview
This document describes the refactoring of #defines and common constants into a single centralized header file for Kyle's Editor (ke).
## Changes Made
### 1. Created `ke_constants.h`
A new header file was created to centralize all common constants and preprocessor definitions used throughout the project.
**Location**: `/Users/kyle/src/ke2/ke_constants.h`
**Contents**:
- **Version Information**: `KE_VERSION` - Editor version string
- **Terminal Sequences**: `ESCSEQ` - ANSI escape sequence prefix
- **Keyboard Macros**: `CTRL_KEY(key)` - Control key combination macro
- **Display Constants**:
- `TAB_STOP` (8) - Tab stop width
- `MSG_TIMEO` (3) - Message timeout in seconds
- **Memory Management**: `INITIAL_CAPACITY` (64) - Initial buffer capacity
- **Keyboard Modes**:
- `MODE_NORMAL` (0) - Normal editing mode
- `MODE_KCOMMAND` (1) - Command mode (^k commands)
- `MODE_ESCAPE` (2) - Escape key handling mode
- **Kill Ring Operations**:
- `KILLRING_NO_OP` (0) - No operation
- `KILLRING_APPEND` (1) - Append deleted characters
- `KILLRING_PREPEND` (2) - Prepend deleted characters
- `KILLING_SET` (3) - Set killring to deleted character
- `KILLRING_FLUSH` (4) - Clear the killring
- **Legacy Initializers**: `ABUF_INIT` - C struct initializer for append buffer
### 2. Updated `main.c`
- Added `#include "ke_constants.h"` at the top with other includes
- Removed all duplicate #define statements that were moved to the constants header
- Eliminated the duplicate `TAB_STOP` definition (was defined twice on lines 36 and 50)
- Cleaned up approximately 30+ lines of redundant definitions
### 3. Updated `erow.cpp`
- Added `#include "ke_constants.h"` with extern "C" linkage for C++ compatibility
- Removed local `constexpr int TAB_STOP = 8` definition
- Now uses the centralized `TAB_STOP` constant from `ke_constants.h`
## Benefits
1. **Single Source of Truth**: All constants are now defined in one location, making them easier to maintain and modify
2. **Eliminated Duplication**: Removed duplicate definitions (e.g., TAB_STOP was defined twice in main.c)
3. **Better Organization**: Constants are grouped logically by category with clear comments
4. **Cross-Language Compatibility**: Header works with both C (main.c) and C++ (erow.cpp) code
5. **Easier Maintenance**: Future changes to constants only need to be made in one place
6. **Improved Readability**: Cleaner source files with less clutter from #define statements
## Build System Compatibility
Both build systems have been tested and verified to work correctly:
### Makefile Build
```bash
make clean
make
```
Successfully compiles all files (main.c, abuf.cpp, erow.cpp) and links the executable.
### CMake Build
```bash
cd cmake-build-debug
cmake --build . --clean-first
```
Successfully builds the project with all components.
## Header Guard
The header uses standard include guards:
```c
#ifndef KE_CONSTANTS_H
#define KE_CONSTANTS_H
...
#endif /* KE_CONSTANTS_H */
```
## C/C++ Interoperability
When including in C++ files, use extern "C" linkage:
```cpp
extern "C" {
#include "ke_constants.h"
}
```
This ensures proper linkage and prevents name mangling issues.
## Future Considerations
1. **Additional Constants**: As the project grows, any new constants should be added to `ke_constants.h` rather than being scattered across source files
2. **Namespacing**: For C++ constants, consider creating a dedicated C++ header with namespaced constants
3. **Type Safety**: For C++ code, consider using `constexpr` or `inline constexpr` variables in a C++ header for better type safety
4. **Documentation**: Keep this document updated as new constants are added or modified
## Testing
All compilation tests passed:
- ✅ Makefile build: Clean compilation with no errors or warnings
- ✅ CMake build: Clean compilation with no errors or warnings
- ✅ All source files (C and C++) successfully include and use the constants
- ✅ No duplicate definition errors
- ✅ Proper linkage between C and C++ components
## Related Files
- `ke_constants.h` - The new constants header (created)
- `main.c` - Updated to use new header (modified)
- `erow.cpp` - Updated to use new header (modified)
- `Makefile` - No changes required (working)
- `CMakeLists.txt` - No changes required (working)
## Date
November 24, 2025

109
docs/CPP17_CONVERSION.md Normal file
View File

@@ -0,0 +1,109 @@
# C++17 Conversion Documentation
## Overview
This document describes the initial conversion of the ke2 project from C to C++17 standards, starting with the `abuf` and `erow` structures as requested.
## Converted Components
### 1. abuf (Append Buffer)
**Original C Structure (main.c:69-73):**
```c
struct abuf {
char *b;
int len;
int cap;
};
```
**C++17 Implementation (abuf.hpp, abuf.cpp):**
- Converted to a modern C++ class using RAII principles
- Uses `std::string` as the internal buffer for automatic memory management
- Implements move semantics (deleted copy operations)
- Uses C++17 features:
- `std::string_view` for efficient string parameters
- `[[nodiscard]]` attributes for better safety
- `noexcept` specifications where appropriate
- No manual memory management required - destructor automatically handled by std::string
**Key Improvements:**
- Automatic memory cleanup (RAII)
- Exception-safe
- No memory leaks possible
- More efficient with move semantics
- Type-safe with modern C++ idioms
### 2. erow (Editor Row)
**Original C Structure (main.c:79-87):**
```c
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
```
**C++17 Implementation (erow.hpp, erow.cpp):**
- Converted to a C++ class with proper encapsulation
- Uses `std::string` for both `line` and `render` buffers
- Implements move semantics (deleted copy operations)
- All manual memory management replaced with RAII
- Retains all original functionality:
- UTF-8 character handling with `mbrtowc` and `wcwidth`
- Tab expansion (TAB_STOP = 8)
- Control character rendering as `\xx`
- Cursor-to-render and render-to-cursor position conversions
**Key Improvements:**
- Automatic memory cleanup for both line and render buffers
- No malloc/free/realloc needed
- Exception-safe operations
- Better encapsulation with private members
- Modern C++ method naming conventions
## Build System Updates
### Makefile
- Added C++ compiler support (g++)
- Added CXXFLAGS with `-std=c++17`
- Modified build rules to compile C and C++ separately
- Updated to link with C++ compiler
- Enhanced clean target to remove object files
### CMakeLists.txt
- Changed project language from `C` to `C CXX`
- Added `CMAKE_CXX_STANDARD 17` and `CMAKE_CXX_STANDARD_REQUIRED ON`
- Added C++ compiler flags matching C flags
- Updated executable target to include `abuf.cpp` and `erow.cpp`
- Maintained backward compatibility with existing C code (main.c)
## Compilation Testing
Both build systems have been tested and verified:
- ✅ Makefile: Successfully compiles with `make`
- ✅ CMake: Successfully configures and builds
- ✅ Object files generated correctly for all C++ sources
- ✅ No compilation warnings or errors
## C++17 Features Used
1. **std::string** - Modern string class with automatic memory management
2. **std::string_view** - Efficient non-owning string references (C++17)
3. **[[nodiscard]]** - Compiler warnings for ignored return values (C++17)
4. **noexcept** - Exception specifications for optimization
5. **default/delete** - Explicit control over special member functions
6. **Move semantics** - Efficient resource transfer
7. **constexpr** - Compile-time constants (TAB_STOP)
## Next Steps
The project now has a foundation for further C++17 conversion:
- Additional structures can be converted following the same patterns
- Consider converting more C code to C++ classes
- Potential for using more STL containers (std::vector for dynamic arrays)
- Opportunity to use smart pointers for other memory management
- Can leverage more C++17 features (std::optional, structured bindings, etc.)
## Compatibility
- The new C++ code coexists with existing C code (main.c)
- Both can be compiled together and linked successfully
- The conversion maintains the original functionality
- No changes required to existing C code at this stage

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

110
editor.c
View File

@@ -1,110 +0,0 @@
/* 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,
.mode = 0,
.killring = NULL,
.kill = 0,
.no_kill = 0,
.dirtyex = 0,
.uarg = 0,
.ucount = 0,
.msgtm = 0,
.buffers = NULL,
.bufcount = 0,
.curbuf = 0,
.bufcap = 0,
};
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 */
/* 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;
/* initialize buffer system on first init */
if (editor.buffers == NULL && editor.bufcount == 0) {
editor.bufcap = 0;
buffers_init();
}
}
void
reset_editor(void)
{
buffer *b = buffer_current();
if (b == NULL) {
return;
}
if (b->row) {
for (size_t i = 0; i < b->nrows; i++) {
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,35 +0,0 @@
#ifndef KE_EDITOR_H
#define KE_EDITOR_H
#include <termios.h>
#include <time.h>
#include "abuf.h"
#include "buffer.h"
struct editor {
size_t rows, cols;
int mode;
abuf *killring;
int kill; /* KILL CHAIN (\m/) */
int no_kill; /* don't kill in delete_row */
int dirtyex;
char msg[80];
int uarg, ucount; /* C-u support */
time_t msgtm;
struct buffer **buffers; /* array of buffers */
size_t bufcount; /* number of buffers */
size_t curbuf; /* current buffer index */
size_t bufcap; /* current buffer capacity */
};
extern struct editor editor;
void editor_set_status(const char *fmt, ...);
void init_editor(void);
void reset_editor(void);
#endif /* KE_EDITOR_H */

214
erow.cc Normal file
View File

@@ -0,0 +1,214 @@
#include "erow.h"
#include <cstring>
#include <cwchar>
#include <cassert>
#include "ke_constants.h"
namespace ke {
// Constructors
erow::erow(std::size_t initial_capacity) {
line_.reserve(initial_capacity);
}
erow::erow(const char* text, std::size_t len) {
line_.assign(text, len);
}
// Set line content
void erow::set_line(const char* data, std::size_t len) {
line_.assign(data, len);
}
// Append string to the line
void erow::append_string(const char* s, std::size_t len) {
line_.append(s, len);
}
// Insert character at position
void erow::insert_char(std::size_t at, char c) {
if (at > line_.size()) {
at = line_.size();
}
line_.insert(at, 1, c);
}
// Delete character at position
void erow::delete_char(std::size_t at) {
if (at < line_.size()) {
line_.erase(at, 1);
}
}
// Helper function for nibble to hex conversion
char erow::nibble_to_hex(char c) {
c &= 0xf;
if (c < 10) {
return static_cast<char>('0' + c);
}
return static_cast<char>('A' + (c - 10));
}
// Update the render string based on the line content
void erow::update() {
int tabs = 0;
int ctrl = 0;
// Count tabs and control characters
for (std::size_t j = 0; j < line_.size(); ++j) {
if (line_[j] == '\t') {
++tabs;
} else if (static_cast<unsigned char>(line_[j]) < 0x20) {
++ctrl;
}
}
// Allocate render buffer
render_.clear();
render_.reserve(line_.size() + (tabs * (TAB_STOP - 1)) + (ctrl * 3) + 1);
// Build the render string
for (std::size_t j = 0; j < line_.size(); ++j) {
if (line_[j] == '\t') {
// Expand tabs to spaces
do {
render_.push_back(' ');
} while ((render_.size() % TAB_STOP) != 0);
} else if (static_cast<unsigned char>(line_[j]) < 0x20) {
// Render control characters as \xx
render_.push_back('\\');
render_.push_back(nibble_to_hex(line_[j] >> 4));
render_.push_back(nibble_to_hex(line_[j] & 0x0f));
} else {
// Leave UTF-8 multibyte bytes untouched
render_.push_back(line_[j]);
}
}
}
// Convert cursor position to render position
int erow::render_to_cursor(int cx) const {
int rx = 0;
std::size_t j = 0;
wchar_t wc;
std::mbstate_t st{};
while (j < static_cast<std::size_t>(cx) && j < line_.size()) {
unsigned char b = static_cast<unsigned char>(line_[j]);
if (b == '\t') {
rx += (TAB_STOP - 1) - (rx % TAB_STOP);
++rx;
++j;
continue;
}
if (b < 0x20) {
// Render as \xx -> width 3
rx += 3;
++j;
continue;
}
std::size_t rem = line_.size() - j;
std::size_t n = std::mbrtowc(&wc, &line_[j], rem, &st);
if (n == static_cast<std::size_t>(-2)) {
// Incomplete sequence at end; treat one byte
rx += 1;
j += 1;
std::memset(&st, 0, sizeof(st));
} else if (n == static_cast<std::size_t>(-1)) {
// Invalid byte; consume one and reset state
rx += 1;
j += 1;
std::memset(&st, 0, sizeof(st));
} else if (n == 0) {
// Null character
rx += 0;
j += 1;
} else {
int w = wcwidth(wc);
if (w < 0) {
w = 1; // Non-printable -> treat as width 1
}
rx += w;
j += n;
}
}
return rx;
}
// Convert render position to cursor position
int erow::cursor_to_render(int rx) const {
int cur_rx = 0;
std::size_t j = 0;
wchar_t wc;
std::mbstate_t st{};
while (j < line_.size()) {
unsigned char b = static_cast<unsigned char>(line_[j]);
if (b == '\t') {
int tab_width = (TAB_STOP - 1) - (cur_rx % TAB_STOP) + 1;
if (cur_rx + tab_width > rx) {
break;
}
cur_rx += tab_width;
++j;
continue;
}
if (b < 0x20) {
// Control char width is 3
if (cur_rx + 3 > rx) {
break;
}
cur_rx += 3;
++j;
continue;
}
std::size_t rem = line_.size() - j;
std::size_t n = std::mbrtowc(&wc, &line_[j], rem, &st);
std::size_t adv = 1;
int w = 1;
if (n == static_cast<std::size_t>(-2)) {
// Incomplete sequence
adv = 1;
w = 1;
std::memset(&st, 0, sizeof(st));
} else if (n == static_cast<std::size_t>(-1)) {
// Invalid byte
adv = 1;
w = 1;
std::memset(&st, 0, sizeof(st));
} else if (n == 0) {
// Null character
adv = 1;
w = 0;
} else {
adv = n;
w = wcwidth(wc);
if (w < 0) {
w = 1;
}
}
if (cur_rx + w > rx) {
break;
}
cur_rx += w;
j += adv;
}
return static_cast<int>(j);
}
} // namespace ke

76
erow.h Normal file
View File

@@ -0,0 +1,76 @@
#ifndef EROW_HPP
#define EROW_HPP
#include <string>
#include <memory>
#include <cstddef>
#include <cstdint>
namespace ke {
/**
* Editor row class representing a line of text with rendering information.
* C++17 implementation with RAII memory management.
*/
class erow {
public:
// Constructors
erow() noexcept = default;
explicit erow(std::size_t initial_capacity);
erow(const char* text, std::size_t len);
// Deleted copy constructor and assignment (use move semantics)
erow(const erow&) = delete;
erow& operator=(const erow&) = delete;
// Move constructor and assignment
erow(erow&&) noexcept = default;
erow& operator=(erow&&) noexcept = default;
// Destructor (automatic cleanup via std::string)
~erow() = default;
// Core operations
void update();
void insert_char(std::size_t at, char c);
void delete_char(std::size_t at);
void append_string(const char* s, std::size_t len);
// Cursor/render position conversions
[[nodiscard]] int render_to_cursor(int cx) const;
[[nodiscard]] int cursor_to_render(int rx) const;
// Accessors for line data
[[nodiscard]] const char* line_data() const noexcept { return line_.data(); }
[[nodiscard]] char* line_data() noexcept { return line_.data(); }
[[nodiscard]] std::size_t line_size() const noexcept { return line_.size(); }
[[nodiscard]] std::size_t line_capacity() const noexcept { return line_.capacity(); }
// Accessors for render data
[[nodiscard]] const char* render_data() const noexcept { return render_.data(); }
[[nodiscard]] std::size_t render_size() const noexcept { return render_.size(); }
// Direct access to strings (for compatibility)
[[nodiscard]] std::string& line() noexcept { return line_; }
[[nodiscard]] const std::string& line() const noexcept { return line_; }
[[nodiscard]] const std::string& render() const noexcept { return render_; }
// Resize operations
void resize_line(std::size_t new_size) { line_.resize(new_size); }
void reserve_line(std::size_t new_cap) { line_.reserve(new_cap); }
// Set line content
void set_line(const char* data, std::size_t len);
void set_line(std::string&& data) noexcept { line_ = std::move(data); }
private:
std::string line_; // The actual line content
std::string render_; // The rendered version (with tabs expanded, etc.)
// Helper for nibble to hex conversion
static char nibble_to_hex(char c);
};
} // namespace ke
#endif // EROW_HPP

180
file_io.cc Normal file
View File

@@ -0,0 +1,180 @@
#include "file_io.h"
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <ctime>
// Need access to main.cc structures and functions
extern void erow_insert(int at, const char *s, int len);
extern void reset_editor();
extern void editor_set_status(const char *fmt, ...);
extern void die(const char *s);
extern char *editor_prompt(const char *, void (*cb)(char *, int16_t));
// erow definition
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void FileIO::open_file(editor_t* editor, const char* filename) {
char* line = nullptr;
size_t linecap = 0;
ssize_t linelen;
FILE* fp = nullptr;
reset_editor();
editor->filename = strdup(filename);
assert(editor->filename != nullptr);
editor->dirty = 0;
if ((fp = fopen(filename, "r")) == nullptr) {
if (errno == ENOENT) {
editor_set_status("[new file]");
return;
}
die("fopen");
}
while ((linelen = getline(&line, &linecap, fp)) != -1) {
if (linelen != -1) {
while ((linelen > 0) && ((line[linelen - 1] == '\r') ||
(line[linelen - 1] == '\n'))) {
linelen--;
}
erow_insert(editor->nrows, line, linelen);
}
}
free(line);
line = nullptr;
fclose(fp);
}
char* FileIO::rows_to_buffer(editor_t* editor, int* buflen) {
int len = 0;
int j;
char* buf = nullptr;
char* p = nullptr;
for (j = 0; j < editor->nrows; j++) {
/* extra byte for newline */
len += editor->row[j].size + 1;
}
if (len == 0) {
return nullptr;
}
*buflen = len;
buf = static_cast<char*>(malloc(len));
assert(buf != nullptr);
p = buf;
for (j = 0; j < editor->nrows; j++) {
memcpy(p, editor->row[j].line, editor->row[j].size);
p += editor->row[j].size;
*p++ = '\n';
}
return buf;
}
int FileIO::save_file(editor_t* editor) {
int fd = -1;
int len;
int status = 1; /* will be used as exit code */
char* buf;
if (!editor->dirty) {
editor_set_status("No changes to save.");
return 0;
}
if (editor->filename == nullptr) {
editor->filename = editor_prompt("Filename: %s", nullptr);
if (editor->filename == nullptr) {
editor_set_status("Save aborted.");
return 0;
}
}
buf = rows_to_buffer(editor, &len);
if ((fd = open(editor->filename, O_RDWR | O_CREAT, 0644)) == -1) {
goto save_exit;
}
if (-1 == ftruncate(fd, len)) {
goto save_exit;
}
if (len == 0) {
status = 0;
goto save_exit;
}
if ((ssize_t) len != write(fd, buf, len)) {
goto save_exit;
}
status = 0;
save_exit:
if (fd)
close(fd);
if (buf) {
free(buf);
buf = nullptr;
}
if (status != 0) {
buf = strerror(errno);
editor_set_status("Error writing %s: %s",
editor->filename,
buf);
} else {
editor_set_status("Wrote %d bytes to %s.",
len,
editor->filename);
editor->dirty = 0;
}
return status;
}
} // namespace ke

29
file_io.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef FILE_IO_HPP
#define FILE_IO_HPP
// Forward declaration
struct editor_t;
namespace ke {
/**
* File I/O class for reading and writing files.
*/
class FileIO {
public:
FileIO() = default;
~FileIO() = default;
// Deleted copy constructor and assignment
FileIO(const FileIO&) = delete;
FileIO& operator=(const FileIO&) = delete;
// File operations
static void open_file(editor_t* editor, const char* filename);
static int save_file(editor_t* editor);
static char* rows_to_buffer(editor_t* editor, int* buflen);
};
} // namespace ke
#endif // FILE_IO_HPP

122
input_handler.cc Normal file
View File

@@ -0,0 +1,122 @@
#include "input_handler.h"
#include "ke_constants.h"
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
namespace ke {
// Key codes for special keys
enum KeyPress {
TAB_KEY = 9,
ESC_KEY = 27,
BACKSPACE = 127,
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PG_UP,
PG_DN,
};
int16_t InputHandler::get_keypress() {
char seq[3];
/* read raw byte so UTF-8 bytes (>=0x80) are not sign-extended */
unsigned char uc = 0;
int16_t c;
if (read(STDIN_FILENO, &uc, 1) == -1) {
perror("get_keypress:read");
exit(1);
}
c = (int16_t) uc;
if (c == 0x1b) {
if (read(STDIN_FILENO, &seq[0], 1) != 1)
return c;
if (read(STDIN_FILENO, &seq[1], 1) != 1)
return c;
if (seq[0] == '[') {
if (seq[1] < 'A') {
if (read(STDIN_FILENO, &seq[2], 1) != 1)
return c;
if (seq[2] == '~') {
switch (seq[1]) {
case '1':
return HOME_KEY;
case '3':
return DEL_KEY;
case '4':
return END_KEY;
case '5':
return PG_UP;
case '6':
return PG_DN;
case '7':
return HOME_KEY;
case '8':
return END_KEY;
}
}
} else {
switch (seq[1]) {
case 'A':
return ARROW_UP;
case 'B':
return ARROW_DOWN;
case 'C':
return ARROW_RIGHT;
case 'D':
return ARROW_LEFT;
case 'F':
return END_KEY;
case 'H':
return HOME_KEY;
default:
/* nada */ ;
}
}
} else if (seq[0] == 'O') {
switch (seq[1]) {
case 'F':
return END_KEY;
case 'H':
return HOME_KEY;
}
}
return 0x1b;
}
return c;
}
bool InputHandler::is_arrow_key(int16_t c) {
switch (c) {
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
case ARROW_UP:
case CTRL_KEY('p'):
case CTRL_KEY('n'):
case CTRL_KEY('f'):
case CTRL_KEY('b'):
case CTRL_KEY('a'):
case CTRL_KEY('e'):
case END_KEY:
case HOME_KEY:
case PG_DN:
case PG_UP:
return true;
}
return false;
}
} // namespace ke

29
input_handler.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef INPUT_HANDLER_HPP
#define INPUT_HANDLER_HPP
#include <cstdint>
namespace ke {
/**
* Input handler class for reading and processing keyboard input.
*/
class InputHandler {
public:
InputHandler() = default;
~InputHandler() = default;
// Deleted copy constructor and assignment
InputHandler(const InputHandler&) = delete;
InputHandler& operator=(const InputHandler&) = delete;
// Read a keypress from stdin
static int16_t get_keypress();
// Check if a key code is an arrow/navigation key
static bool is_arrow_key(int16_t c);
};
} // namespace ke
#endif // INPUT_HANDLER_HPP

42
ke.1
View File

@@ -22,21 +22,11 @@ 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 b
Switch to a buffer.
.It C-k c
Close the current buffer. If no other buffers are open, an empty
buffer will be opened. To exit, use C-k q.
.It C-k d
Delete from the cursor to the end of the line.
.It C-k C-d
@@ -44,52 +34,34 @@ Delete the entire line.
.It C-k e
Edit a new file. Also C-k C-e.
.It C-k f
Flush the kill ring.
Incremental find.
.It C-k g
Go to a specific line.
.It C-k j
Jump to the mark.
Go to a specific line. Also C-k C-g.
.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 p
Switch to the next buffer.
.It C-k 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.
exit the editor. Also C-k C-q.
.It C-k s
Save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k u
Undo changes (not implemented; marking this k-command as taken).
.It C-k U
Redo changes (not implemented; marking this k-command as taken).
save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k x
save the file and exit. Also C-k C-x.
.It C-k y
Yank the kill ring.
Yank the killring.
.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 kill ring.
Yank the killring.
.It ESC BACKSPACE
Delete the previous word.
.It ESC b
@@ -99,7 +71,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 kill ring.
Save the region (if the mark is set) to the killring.
.It
.El
.Sh FIND

44
ke_constants.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* ke_constants.h
*
* Common constants and defines for Kyle's Editor (ke)
* Refactored from main.c and other source files for centralized maintenance.
*/
#ifndef KE_CONSTANTS_H
#define KE_CONSTANTS_H
/* Version information */
#ifndef KE_VERSION
#define KE_VERSION "ke dev build"
#endif
/* Terminal escape sequences */
#define ESCSEQ "\x1b["
/* Keyboard and control key macros */
#define CTRL_KEY(key) ((key)&0x1f)
/* Display and rendering constants */
#define TAB_STOP 8
#define MSG_TIMEO 3
/* Memory management constants */
#define INITIAL_CAPACITY 64
/* Keyboard input modes */
#define MODE_NORMAL 0
#define MODE_KCOMMAND 1
#define MODE_ESCAPE 2
/* Kill ring operations */
#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 */
/* Legacy C struct initializers (for compatibility with main.c) */
#define ABUF_INIT {NULL, 0, 0}
#endif /* KE_CONSTANTS_H */

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.");
}

262
killring.cc Normal file
View File

@@ -0,0 +1,262 @@
#include "killring.h"
#include <termios.h>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <ctime>
// Need access to main.cc structures and functions
extern int erow_init(struct erow *row, int len);
extern void erow_update(struct erow *row);
extern void erow_free(struct erow *row);
extern void editor_set_status(const char *fmt, ...);
extern void insertch(int16_t c);
extern void newline();
extern void move_cursor(int16_t c);
extern int cursor_at_eol();
extern void swap_int(int *a, int *b);
extern void deletech(uint8_t op);
// erow definition
struct erow {
char *line;
char *render;
int size;
int rsize;
int cap;
};
// editor_t definition
struct editor_t {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
struct erow *row;
struct erow *killring;
int kill;
int no_kill;
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
time_t msgtm;
};
namespace ke {
void Killring::flush(editor_t* editor) {
if (editor->killring != nullptr) {
erow_free(editor->killring);
free(editor->killring);
editor->killring = nullptr;
}
}
void Killring::yank(editor_t* editor) {
if (editor->killring == nullptr) {
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 < editor->killring->size; i++) {
unsigned char ch = (unsigned char) editor->killring->line[i];
if (ch == '\n') {
newline();
} else {
insertch(ch);
}
}
}
void Killring::start_with_char(editor_t* editor, unsigned char ch) {
erow* row = nullptr;
if (editor->killring != nullptr) {
erow_free(editor->killring);
free(editor->killring);
editor->killring = nullptr;
}
editor->killring = static_cast<erow*>(malloc(sizeof(erow)));
assert(editor->killring != nullptr);
assert(erow_init(editor->killring, 0) == 0);
/* append one char to empty killring without affecting editor.dirty */
row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
}
void Killring::append_char(editor_t* editor, unsigned char ch) {
erow* row = nullptr;
if (editor->killring == nullptr) {
start_with_char(editor, ch);
return;
}
row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
row->line[row->size] = ch;
row->size++;
row->line[row->size] = '\0';
erow_update(row);
}
void Killring::prepend_char(editor_t* editor, unsigned char ch) {
if (editor->killring == nullptr) {
start_with_char(editor, ch);
return;
}
erow* row = editor->killring;
row->line = static_cast<char*>(realloc(row->line, row->size + 2));
assert(row->line != nullptr);
memmove(&row->line[1], &row->line[0], row->size + 1);
row->line[0] = ch;
row->size++;
erow_update(row);
}
void Killring::toggle_markset(editor_t* editor) {
if (editor->mark_set) {
editor->mark_set = 0;
editor_set_status("Mark cleared.");
return;
}
editor->mark_set = 1;
editor->mark_curx = editor->curx;
editor->mark_cury = editor->cury;
editor_set_status("Mark set.");
}
int Killring::cursor_after_mark(editor_t* editor) {
if (editor->mark_cury < editor->cury) {
return 1;
}
if (editor->mark_cury > editor->cury) {
return 0;
}
return editor->curx >= editor->mark_curx;
}
int Killring::count_chars_from_cursor_to_mark(editor_t* editor) {
int count = 0;
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (editor->cury != cury) {
while (!cursor_at_eol()) {
move_cursor(1000 + 3); // ARROW_RIGHT
count++;
}
move_cursor(1000 + 3); // ARROW_RIGHT
count++;
}
while (editor->curx != curx) {
count++;
move_cursor(1000 + 3); // ARROW_RIGHT
}
return count;
}
void Killring::kill_region(editor_t* editor) {
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!editor->mark_set) {
return;
}
/* kill the current killring */
flush(editor);
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (editor->cury != cury) {
while (!cursor_at_eol()) {
append_char(editor, editor->row[editor->cury].line[editor->curx]);
move_cursor(1000 + 3); // ARROW_RIGHT
}
append_char(editor, '\n');
move_cursor(1000 + 3); // ARROW_RIGHT
}
while (editor->curx != curx) {
append_char(editor, editor->row[editor->cury].line[editor->curx]);
move_cursor(1000 + 3); // ARROW_RIGHT
}
editor_set_status("Region killed.");
}
void Killring::delete_region(editor_t* editor) {
int count = count_chars_from_cursor_to_mark(editor);
int killed = 0;
int curx = editor->curx;
int cury = editor->cury;
int markx = editor->mark_curx;
int marky = editor->mark_cury;
if (!editor->mark_set) {
return;
}
if (!cursor_after_mark(editor)) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor->curx = markx;
editor->cury = marky;
while (killed < count) {
deletech(0); // KILLRING_NO_OP
killed++;
}
editor->kill = 1;
editor_set_status("Region killed.");
}
} // namespace ke

View File

@@ -1,26 +1,44 @@
#ifndef KE_KILLRING_H
#define KE_KILLRING_H
#ifndef KILLRING_HPP
#define KILLRING_HPP
#include <cstdint>
#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 */
// Forward declarations
struct editor_t;
namespace ke {
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);
/**
* Killring class for cut/paste (kill/yank) operations.
*/
class Killring {
public:
Killring() = default;
~Killring() = default;
// Deleted copy constructor and assignment
Killring(const Killring&) = delete;
Killring& operator=(const Killring&) = delete;
#endif
// Killring operations
static void flush(editor_t* editor);
static void yank(editor_t* editor);
static void start_with_char(editor_t* editor, unsigned char ch);
static void append_char(editor_t* editor, unsigned char ch);
static void prepend_char(editor_t* editor, unsigned char ch);
// Mark operations
static void toggle_markset(editor_t* editor);
static int cursor_after_mark(editor_t* editor);
// Region operations
static void kill_region(editor_t* editor);
static void delete_region(editor_t* editor);
private:
static int count_chars_from_cursor_to_mark(editor_t* editor);
};
} // namespace ke
#endif // KILLRING_HPP

238
main.c
View File

@@ -1,238 +0,0 @@
/*
* ke - kyle's editor
*
* ke started off following along with the kilo walkthrough at
* https://viewsourcecode.org/snaptoken/kilo/
*
* It is inspired heavily by mg(1) and VDE. This is a single file and
* can be built with
* $(CC) -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -Wall -Wextra -pedantic \
* -Wshadow -Werror -std=c99 -g -o ke main.c
*
* It builds and runs on Linux and Darwin. I can't confirm BSD compatibility.
*
* commit 59d3fa1dab68e8683d5f5a9341f5f42ef3308876
* Author: Kyle Isom <kyle@imap.cc>
* Date: Fri Feb 7 20:46:43 2020 -0800
*
* Initial import, starting with kyle's editor.
*/
#include <sys/ioctl.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <sys/stat.h>
#include "abuf.h"
#include "buffer.h"
#include "editor.h"
#include "editing.h"
#include "process.h"
#include "term.h"
void loop(void);
void enable_debugging(void);
void deathknell(void);
static void signal_handler(int sig);
static void install_signal_handlers(void);
int
kbhit(void)
{
int bytes_waiting = 0;
ioctl(STDIN_FILENO, FIONREAD, &bytes_waiting);
if (bytes_waiting < 0) {
editor_set_status("kbhit: FIONREAD failed: %s", strerror(errno));
/* if FIONREAD fails, we need to assume we should read. this
* will default to a much slower input sequence, but it'll work.
*/
return 1;
}
return bytes_waiting > 0;
}
void
loop(void)
{
int up = 1; /* update on the first runthrough */
while (1) {
if (up) {
display_refresh();
}
/*
* ke should only refresh the display if it has received keyboard
* input; if it has, drain all the inputs. This is useful for
* handling pastes without massive screen flicker.
*
*/
up = process_keypress();
if (up != 0) {
while (kbhit()) {
process_keypress();
}
}
}
}
void
enable_debugging(void)
{
dump_pidfile();
}
void
deathknell(void)
{
fflush(stderr);
if (editor.killring != NULL) {
ab_free(editor.killring);
free(editor.killring);
editor.killring = NULL;
}
reset_editor();
disable_termraw();
}
static void
signal_handler(int sig)
{
signal(sig, SIG_DFL);
fprintf(stderr, "caught signal %d\n", sig);
deathknell();
raise(sig);
_exit(127 + sig);
}
static void
install_signal_handlers(void)
{
/* Block all these signals while inside any of them */
const int fatal_signals[] = {
SIGABRT, SIGFPE, SIGILL, SIGSEGV,
#ifdef SIGBUS
SIGBUS,
#endif
#ifdef SIGQUIT
SIGQUIT,
#endif
#ifdef SIGSYS
SIGSYS,
#endif
-1 /* sentinel */
};
int i = 0;
for (i = 0; fatal_signals[i] != -1; i++) {
signal(fatal_signals[i], signal_handler);
}
atexit(deathknell);
}
int
main(int argc, char *argv[])
{
const char *arg = NULL;
const char *path = NULL;
int i = 0;
int v = 0;
int nb = 0;
int opt = 0;
int debug = 0;
int pending_line = 0; /* line number for the next filename */
int first_loaded = 0; /* has a filed been loaded already? */
install_signal_handlers();
while ((opt = getopt(argc, argv, "df:")) != -1) {
if (opt == 'd') {
debug = 1;
} else {
fprintf(stderr, "Usage: ke [-d] [-f logfile] [ +N ] [file ...]\n");
exit(EXIT_FAILURE);
}
}
argc -= optind;
argv += optind;
setlocale(LC_ALL, "");
if (debug) {
enable_debugging();
}
setup_terminal();
init_editor();
/* start processing file names. if an arg starts with a '+',
* interpret it as the line to jump to.
*/
for (i = 0; i < argc; i++) {
arg = argv[i];
if (arg[0] == '+') {
path = arg + 1;
v = 0;
if (*path != '\0') {
v = atoi(path);
if (v < 1) v = 0;
}
pending_line = v;
continue;
}
if (!first_loaded) {
open_file(arg);
if (pending_line > 0) {
jump_to_position(0, pending_line - 1);
pending_line = 0;
}
first_loaded = 1;
} else {
nb = buffer_add_empty();
buffer_switch(nb);
open_file(arg);
if (pending_line > 0) {
jump_to_position(0, pending_line - 1);
pending_line = 0;
}
}
}
editor_set_status("C-k q to exit / C-k d to dump core");
display_clear(NULL);
loop();
return 0;
}

2531
main.cc Normal file

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

206
scratch.c
View File

@@ -1,206 +0,0 @@
/*
* scratch.c - ideas in progress
*/
#include "buffer.h"
#include "abuf.h"
#include <ctype.h>
#include <string.h>
#define REFLOW_MARGIN 72
void
reflow_region(void)
{
int start_row, end_row, i, col, wlen, this_len;
abuf *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 (EMARK_SET) {
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;
}
} else {
start_row = end_row = ECURY;
while (start_row > 0 && EROW[start_row - 1].size > 0) {
start_row--;
}
while (end_row < ENROWS - 1 &&
EROW[end_row + 1].size > 0) {
end_row++;
}
}
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) {
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 < (int)row->size &&
(row->b[indent_len] == ' ' ||
row->b[indent_len] == '\t')) {
indent[indent_len] = row->b[indent_len], indent_len++;
}
indent[indent_len] = '\0';
in_paragraph = 1;
}
ab_append(&buf, row->b + 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);
EDIRTY++;
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();
}

312
term.c
View File

@@ -1,312 +0,0 @@
#include <sys/ioctl.h>
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "abuf.h"
#include "core.h"
#include "buffer.h"
#include "editing.h"
#include "editor.h"
#include "process.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(size_t *rows, size_t *cols)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
}
*cols = (size_t)ws.ws_col;
*rows = (size_t)ws.ws_row;
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);
}

30
term.h
View File

@@ -1,30 +0,0 @@
#ifndef KE_TERM_H
#define KE_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);
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.
*
* 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(size_t *rows, size_t *cols);
#endif /* KE_TERM_H */

111
terminal.cc Normal file
View File

@@ -0,0 +1,111 @@
#include "terminal.h"
#include <sys/ioctl.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
namespace ke {
static Terminal* g_terminal_instance = nullptr;
static void cleanup_terminal_on_exit() {
if (g_terminal_instance) {
g_terminal_instance->disable_raw_mode();
}
}
Terminal::Terminal() : raw_mode_enabled_(false) {
g_terminal_instance = this;
}
Terminal::~Terminal() {
if (raw_mode_enabled_) {
disable_raw_mode();
}
g_terminal_instance = nullptr;
}
void Terminal::setup() {
if (tcgetattr(STDIN_FILENO, &entry_term_) == -1) {
perror("can't snapshot terminal settings");
exit(1);
}
atexit(cleanup_terminal_on_exit);
enable_raw_mode();
}
void Terminal::enable_raw_mode() {
if (raw_mode_enabled_) {
return;
}
struct termios raw;
/* Read the current terminal parameters for standard input. */
if (tcgetattr(STDIN_FILENO, &raw) == -1) {
perror("tcgetattr while enabling raw mode");
exit(1);
}
/*
* Put the terminal into raw mode.
*/
cfmakeraw(&raw);
/*
* Set timeout for read(2).
*
* VMIN: what is the minimum number of bytes required for read
* to return?
*
* VTIME: max time before read(2) returns in hundreds of milli-
* seconds.
*/
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
/*
* Now write the terminal parameters to the current terminal,
* after flushing any waiting input out.
*/
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {
perror("tcsetattr while enabling raw mode");
exit(1);
}
raw_mode_enabled_ = true;
}
void Terminal::disable_raw_mode() {
if (!raw_mode_enabled_) {
return;
}
clear_screen();
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &entry_term_) == -1) {
perror("couldn't disable terminal raw mode");
exit(1);
}
raw_mode_enabled_ = false;
}
int Terminal::get_window_size(int* rows, int* cols) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
}
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
void Terminal::clear_screen() {
(void)write(STDOUT_FILENO, "\x1b[2J", 4);
(void)write(STDOUT_FILENO, "\x1b[H", 3);
}
} // namespace ke

39
terminal.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef TERMINAL_HPP
#define TERMINAL_HPP
#include <termios.h>
namespace ke {
/**
* Terminal management class for handling raw mode and terminal operations.
*/
class Terminal {
public:
Terminal();
~Terminal();
// Deleted copy constructor and assignment
Terminal(const Terminal&) = delete;
Terminal& operator=(const Terminal&) = delete;
// Setup and teardown
void setup();
void enable_raw_mode();
void disable_raw_mode();
// Terminal operations
static int get_window_size(int* rows, int* cols);
static void clear_screen();
// Access to original terminal settings
const termios& entry_term() const { return entry_term_; }
private:
termios entry_term_;
bool raw_mode_enabled_;
};
} // namespace ke
#endif // TERMINAL_HPP

240
undo.c
View File

@@ -1,240 +0,0 @@
/*
* undo.c: ke's undo system
*/
#include <assert.h>
#include <stdlib.h>
#include "abuf.h"
#include "buffer.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;
ab_init(&node->text);
node->next = NULL;
node->next = NULL;
return node;
}
void
undo_node_free(undo_node *node)
{
if (node == NULL) {
return;
}
ab_free(&node->text);
}
void
undo_node_free_all(undo_node *node)
{
undo_node *next = NULL;
if (node == NULL) {
return;
}
while (node != NULL) {
next = node->next;
undo_node_free(node);
free(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_node_new(kind);
assert(pending != NULL);
tree->pending = pending;
}
void
undo_prepend(undo_tree *tree, abuf *buf)
{
assert(tree != NULL);
assert(tree->pending != NULL);
ab_prepend_ab(&tree->pending->text, buf);
}
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) {
assert(tree->current == NULL);
tree->root = tree->pending;
tree->current = tree->pending;
tree->pending = NULL;
return;
}
assert(tree->current != NULL);
if (tree->current->next != NULL) {
undo_node_free_all(tree->current->next);
}
tree->pending->prev = tree->current;
tree->current->next = tree->pending;
tree->current = tree->pending;
tree->pending = NULL;
}
static int
undo_apply_undo(struct buffer *buf)
{
undo_node *node = buf->undo.current;
switch (node->kind) {
case UNDO_INSERT:
break;
default:
return 0;
}
return 0;
}
int
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)
{
if (buf == NULL) {
return 0;
}
undo_commit(&buf->undo);
return undo_apply(buf, UNDO_DIR_UNDO);
}
int
editor_redo(struct buffer *buf)
{
if (buf == NULL) {
return 0;
}
undo_commit(&buf->undo);
return undo_apply(buf, UNDO_DIR_REDO);
}

56
undo.h
View File

@@ -1,56 +0,0 @@
#include <stddef.h>
#include "abuf.h"
#ifndef KE_UNDO_H
#define KE_UNDO_H
struct buffer;
#define UNDO_DIR_UNDO -1
#define UNDO_DIR_REDO 1
typedef enum undo_kind {
UNDO_INSERT = 1 << 0,
UNDO_UNKNOWN = 1 << 1,
} undo_kind;
typedef struct undo_node {
undo_kind kind;
size_t row, col;
abuf text;
struct undo_node *next;
struct undo_node *prev;
} 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(undo_tree *tree, abuf *buf);
void undo_append(undo_tree *tree, abuf *buf);
void undo_prependch(undo_tree *tree, char c);
void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree);
int undo_apply(struct buffer *buf, int direction);
int editor_undo(struct buffer *buf);
int editor_redo(struct buffer *buf);
#endif