Files
ke/main.c

3026 lines
56 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.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 <wctype.h>
#include <sys/stat.h>
#include <dirent.h>
#include <limits.h>
#include "abuf.h"
#include "buffer.h"
#include "core.h"
#include "term.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 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
#define TAB_STOP 8
#define INITIAL_CAPACITY 64
#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 */
/*
* editor is the global editor state.
*/
struct editor {
struct termios entry_term;
int rows, cols;
int curx, cury;
int rx;
int mode;
int nrows;
int rowoffs, coloffs;
abuf *row;
abuf *killring;
int kill; /* KILL CHAIN (sounds metal) */
int no_kill; /* don't kill in delete_row */
char *filename;
int dirty;
int dirtyex;
char msg[80];
int mark_set;
int mark_curx, mark_cury;
int uarg, ucount; /* C-u support */
time_t msgtm;
/* Multi-buffer support */
struct buffer **buffers; /* buffer list */
int bufcount; /* number of buffers */
int curbuf; /* current buffer index */
} editor = {
.cols = 0,
.rows = 0,
.curx = 0,
.cury = 0,
.mode = 0,
.nrows = 0,
.rowoffs = 0,
.coloffs = 0,
.row = NULL,
.killring = NULL,
.kill = 0,
.no_kill = 0,
.filename = NULL,
.dirty = 0,
.dirtyex = 0,
.mark_set = 0,
.mark_curx = 0,
.mark_cury = 0,
.uarg = 0,
.ucount = 0,
.buffers = NULL,
.bufcount = 0,
.curbuf = -1,
};
void init_editor(void);
void reset_editor(void);
/* buffers now declared in buffer.h */
/* small tools, abufs, etc */
int next_power_of_2(int n);
int cap_growth(int cap, int sz);
size_t kstrnlen(const char *buf, size_t max);
void ab_init(abuf *buf);
void ab_init_cap(abuf *buf, size_t cap);
char nibble_to_hex(char c);
void swap_int(int *a, int *b);
/* kill ring, marking, etc... */
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 delete_region(void);
/* miscellaneous */
void kwrite(int fd, const char *buf, int len);
void die(const char *s);
/* get_winsz now provided by term.h */
void jump_to_position(int col, int row);
void goto_line(void);
int cursor_at_eol(void);
int iswordchar(unsigned char c);
void find_next_word(void);
void delete_next_word(void);
void find_prev_word(void);
void delete_prev_word(void);
void delete_row(int at);
void row_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);
void editor_find_callback(char *query, int16_t c);
void editor_find(void);
char *editor_prompt(char*, void (*cb)(char*, int16_t));
void editor_openfile(void);
int first_nonwhitespace(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);
void process_kcommand(int16_t c);
void process_normal(int16_t c);
void process_escape(int16_t c);
int process_keypress(void);
char *get_cloc_code_lines(const char *filename);
int dump_pidfile(void);
/* terminal functions declared in term.h */
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);
void editor_set_status(const char *fmt, ...);
void loop(void);
void enable_debugging(void);
void deathknell(void);
static void signal_handler(int sig);
static void install_signal_handlers(void);
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);
}
/*
* init_editor should set up the global editor struct.
*/
void
init_editor(void)
{
editor.cols = 0;
editor.rows = 0;
if (get_winsz(&editor.rows, &editor.cols) == -1) {
die("can't get window size");
}
editor.rows--; /* status bar */
editor.rows--; /* message line */
editor.curx = editor.cury = 0;
editor.rx = 0;
editor.nrows = 0;
editor.rowoffs = editor.coloffs = 0;
editor.row = NULL;
/* don't clear out the kill ring:
* killing / yanking across files is helpful, and killring
* is initialized to NULL at program start.
*/
/* editor.killring = NULL; */
editor.kill = 0;
editor.no_kill = 0;
editor.msg[0] = '\0';
editor.msgtm = 0;
editor.dirty = 0;
editor.mark_set = 0;
editor.mark_cury = editor.mark_curx = 0;
/* initialize buffer system on first init */
if (editor.buffers == NULL && editor.bufcount == 0) {
buffers_init();
}
}
/*
* reset_editor presumes that editor has been initialized.
*/
void
reset_editor(void)
{
/* Clear current working set (does not reset terminal or buffers list) */
for (int i = 0; i < editor.nrows; i++) {
ab_free(&editor.row[i]);
}
free(editor.row);
editor.row = NULL;
editor.nrows = 0;
editor.rowoffs = editor.coloffs = 0;
editor.curx = editor.cury = 0;
editor.rx = 0;
if (editor.filename != NULL) {
free(editor.filename);
editor.filename = NULL;
}
editor.dirty = 0;
editor.mark_set = 0;
editor.mark_cury = editor.mark_curx = 0;
}
void
ab_init(abuf *buf)
{
buf->b = NULL;
buf->size = 0;
buf->cap = 0;
}
void
ab_init_cap(abuf *buf, const size_t cap)
{
buf->b = calloc1(cap);
buf->size = 0;
buf->cap = cap;
}
void
ab_resize(abuf *buf, size_t cap)
{
cap = cap_growth(buf->cap, cap);
buf->b = realloc(buf->b, cap);
assert(buf->b != NULL);
buf->cap = cap;
}
/* Buffer management moved to buffer.c */
/* =========================
* File open: TAB completion callback
* ========================= */
static 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;
}
static size_t str_lcp2(const char *a, const char *b)
{
if (!a || !b) return 0;
size_t i = 0;
while (a[i] && b[i] && a[i] == b[i]) i++;
return i;
}
static void file_open_prompt_cb(char *buf, int16_t key)
{
if (key != TAB_KEY) return;
/* Determine directory and basename prefix */
char dirpath[PATH_MAX];
char base[256];
const char *slash = strrchr(buf, '/');
if (slash) {
size_t dlen = (size_t)(slash - buf);
if (dlen == 0) {
/* path like "/foo" -> dir is "/" */
strcpy(dirpath, "/");
} else {
if (dlen >= sizeof(dirpath)) dlen = sizeof(dirpath) - 1;
memcpy(dirpath, buf, dlen);
dirpath[dlen] = '\0';
}
strncpy(base, slash + 1, sizeof(base) - 1);
base[sizeof(base) - 1] = '\0';
} else {
strcpy(dirpath, ".");
strncpy(base, buf, sizeof(base) - 1);
base[sizeof(base) - 1] = '\0';
}
DIR *d = opendir(dirpath);
if (!d) {
editor_set_status("No such dir: %s", dirpath);
return;
}
/* Collect matches */
const char *names[128];
int isdir[128];
int n = 0;
size_t plen = strlen(base);
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
/* Skip . and .. */
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (plen == 0 || strncmp(name, base, plen) == 0) {
if (n < 128) {
names[n] = strdup(name);
/* Build full path to test dir */
char full[PATH_MAX];
if (snprintf(full, sizeof(full), "%s/%s", dirpath, name) >= 0) {
isdir[n] = path_is_dir(full);
} else {
isdir[n] = 0;
}
n++;
}
}
}
closedir(d);
if (n == 0) {
editor_set_status("No file matches '%s' in %s", base, dirpath);
return;
}
/* Compute LCP across matches */
size_t lcp = strlen(names[0]);
for (int i = 1; i < n; i++) {
size_t k = str_lcp2(names[0], names[i]);
if (k < lcp) lcp = k;
if (lcp == 0) break;
}
/* Build new buffer string: dirpath + '/' (if not root and present) + completion */
char newbuf[PATH_MAX];
newbuf[0] = '\0';
if (slash) {
/* Preserve original directory portion including trailing slash */
size_t dlen = (size_t)(slash - buf);
if (dlen >= sizeof(newbuf)) dlen = sizeof(newbuf) - 1;
memcpy(newbuf, buf, dlen);
newbuf[dlen] = '\0';
strncat(newbuf, "/", sizeof(newbuf) - strlen(newbuf) - 1);
}
/* The part to append is: if unique -> full name (+ '/' if dir), else current base extended to LCP */
if (n == 1) {
strncat(newbuf, names[0], sizeof(newbuf) - strlen(newbuf) - 1);
if (isdir[0]) {
strncat(newbuf, "/", sizeof(newbuf) - strlen(newbuf) - 1);
}
/* Replace buffer */
strncpy(buf, newbuf[0] ? newbuf : names[0], 127);
buf[127] = '\0';
editor_set_status("Unique match: %s%s", names[0], isdir[0] ? "/" : "");
} else {
/* Extend to LCP */
char ext[256];
size_t cur = strlen(base);
if (lcp > cur) {
size_t to_copy = lcp - cur;
if (to_copy >= sizeof(ext)) to_copy = sizeof(ext) - 1;
memcpy(ext, names[0] + cur, to_copy);
ext[to_copy] = '\0';
/* Always start from current base prefix */
strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1);
strncat(newbuf, ext, sizeof(newbuf) - strlen(newbuf) - 1);
} else {
/* No extension possible, keep base as-is */
strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1);
}
strncpy(buf, newbuf, 127);
buf[127] = '\0';
/* Show candidates */
char msg[80];
size_t used = 0;
used += snprintf(msg + used, sizeof(msg) - used, "%d matches: ", n);
for (int i = 0; i < n && used < sizeof(msg) - 1; i++) {
used += snprintf(msg + used, sizeof(msg) - used, "%s%s%s",
(i ? ", " : ""), names[i], isdir[i] ? "/" : "");
}
editor_set_status("%s", msg);
}
/* Free duplicated names */
for (int i = 0; i < n; i++) free((void*)names[i]);
}
/* Close the current buffer. If dirty, require confirmation (press C-k c twice) */
/* buffer_close_current now implemented in buffer.c */
/* abuf implementations moved to abuf.c */
char
nibble_to_hex(char c)
{
c &= 0xf;
if (c < 10) {
return (char)('0' + c);
}
return (char)('A' + (c - 10));
}
void
swap_int(int *a, int *b)
{
*a ^= *b;
*b ^= *a;
*a ^= *b;
}
int
erow_render_to_cursor(abuf *row, int cx)
{
int rx = 0;
size_t j = 0;
wchar_t wc;
mbstate_t st;
memset(&st, 0, sizeof(st));
while (j < (size_t)cx && j < (size_t)row->size) {
unsigned char b = (unsigned char)row->b[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;
}
if (b < 0x80) {
rx++;
j++;
continue;
}
size_t rem = (size_t)row->size - j;
size_t n = mbrtowc(&wc, &row->b[j], rem, &st);
if (n == (size_t)-2) {
/* incomplete sequence at end; treat one byte */
rx += 1;
j += 1;
memset(&st, 0, sizeof(st));
} else if (n == (size_t)-1) {
/* invalid byte; consume one and reset state */
rx += 1;
j += 1;
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;
}
int
erow_cursor_to_render(abuf *row, int rx)
{
int cur_rx = 0;
size_t j = 0;
wchar_t wc;
mbstate_t st;
memset(&st, 0, sizeof(st));
while (j < (size_t)row->size) {
int w = 0;
size_t adv = 1;
unsigned char b = (unsigned char)row->b[j];
if (b == '\t') {
int add = (TAB_STOP - 1) - (cur_rx % TAB_STOP);
w = add + 1;
adv = 1;
/* tabs are single byte */
} else if (b < 0x20) {
w = 3; /* "\\xx" */
adv = 1;
} else if (b < 0x80) {
w = 1;
adv = 1;
} else {
size_t rem = (size_t)row->size - j;
size_t n = mbrtowc(&wc, &row->b[j], rem, &st);
if (n == (size_t)-2 || n == (size_t)-1) {
/* invalid/incomplete */
w = 1;
adv = 1;
memset(&st, 0, sizeof(st));
} else if (n == 0) {
w = 0;
adv = 1;
} else {
int ww = wcwidth(wc);
if (ww < 0)
ww = 1;
w = ww;
adv = n;
}
}
if (cur_rx + w > rx) {
break;
}
cur_rx += w;
j += adv;
}
return (int)j;
}
int
erow_init(abuf *row, int len)
{
ab_init_cap(row, len);
return 0;
}
void
erow_insert(int at, char *s, int len)
{
abuf *row = realloc(editor.row, sizeof(abuf)*(editor.nrows+1));
assert(row != NULL);
editor.row = row;
if (at < editor.nrows) {
memmove(&editor.row[at+1], &editor.row[at],
sizeof(abuf)*(editor.nrows-at));
}
ab_init(&editor.row[at]);
ab_append(&editor.row[at], s, len);
editor.nrows++;
}
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 (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
cursor_after_mark(void)
{
if (editor.mark_cury < editor.cury) {
return 1;
}
if (editor.mark_cury > editor.cury) {
return 0;
}
return editor.curx >= editor.mark_curx;
}
int
count_chars_from_cursor_to_mark(void)
{
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()) {
swap_int(&curx, &markx);
swap_int(&curx, &marky);
}
editor.curx = markx;
editor.cury = marky;
while (editor.cury != cury) {
while (!cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
count++;
}
move_cursor(ARROW_RIGHT, 1);
count++;
}
while (editor.curx != curx) {
count++;
move_cursor(ARROW_RIGHT, 1);
}
return count;
}
void
kill_region(void)
{
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 */
killring_flush();
if (!cursor_after_mark()) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
editor.curx = markx;
editor.cury = marky;
while (editor.cury != cury) {
while (!cursor_at_eol()) {
killring_append_char(editor.row[editor.cury].b[editor.curx]);
move_cursor(ARROW_RIGHT, 0);
}
killring_append_char('\n');
move_cursor(ARROW_RIGHT, 0);
}
while (editor.curx != curx) {
killring_append_char(editor.row[editor.cury].b[editor.curx]);
move_cursor(ARROW_RIGHT, 0);
}
editor_set_status("Region killed.");
/* clearing the mark needs to be done outside this function; *
* when deleteing the region, the mark needs to be set too. */
}
void
indent_region(void)
{
int start_row, end_row;
int i;
if (!editor.mark_set) {
return;
}
if (editor.mark_cury < editor.cury) {
start_row = editor.mark_cury;
end_row = editor.cury;
} else if (editor.mark_cury > editor.cury) {
start_row = editor.cury;
end_row = editor.mark_cury;
} else {
start_row = end_row = editor.cury;
}
/* Ensure bounds are valid */
if (start_row < 0) {
start_row = 0;
}
if (end_row >= editor.nrows) {
end_row = editor.nrows - 1;
}
if (start_row >= editor.nrows || end_row < 0) {
return;
}
/* Prepend a tab character to every row in the region */
for (i = start_row; i <= end_row; i++) {
row_insert_ch(&editor.row[i], 0, '\t');
}
/* Move cursor to beginning of the line it was on */
editor.curx = 0;
editor.dirty++;
}
void
unindent_region(void)
{
int start_row, end_row, i, del;
abuf *row;
if (!editor.mark_set) {
editor_set_status("Mark not set.");
return;
}
if (editor.mark_cury < editor.cury ||
(editor.mark_cury == editor.cury && editor.mark_curx < editor.curx)) {
start_row = editor.mark_cury;
end_row = editor.cury;
} else {
start_row = editor.cury;
end_row = editor.mark_cury;
}
if (start_row >= editor.nrows) {
return;
}
if (end_row >= editor.nrows) {
end_row = editor.nrows - 1;
}
/* actually unindent every line in the region */
for (i = start_row; i <= end_row; i++) {
row = &editor.row[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 < (int)row->size &&
row->b[del] == ' ') {
del++;
}
if (del > 0) {
memmove(row->b, row->b + del, row->size - del + 1); /* +1 for NUL */
row->size -= del;
}
}
}
editor.curx = 0;
editor.cury = start_row;
editor.dirty++;
editor_set_status("Region unindented");
}
/* call after kill_region */
void
delete_region(void)
{
int count = count_chars_from_cursor_to_mark();
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()) {
swap_int(&curx, &markx);
swap_int(&cury, &marky);
}
jump_to_position(markx, marky);
while (killed < count) {
move_cursor(ARROW_RIGHT, 0);
deletech(KILLRING_NO_OP);
killed++;
}
while (editor.curx != markx && editor.cury != marky) {
deletech(KILLRING_NO_OP);
}
editor.kill = 1;
editor_set_status("Region killed.");
}
void
jump_to_position(int col, int row)
{
/* clamp position */
if (row < 0) {
row = 0;
} else if (row > editor.nrows) {
row = editor.nrows - 1;
}
if (col < 0) {
col = 0;
} else if (col > (int)editor.row[row].size) {
col = (int)editor.row[row].size;
}
editor.curx = col;
editor.cury = row;
display_refresh();
}
void
goto_line(void)
{
int lineno = 0;
char *query = editor_prompt("Line: %s", NULL);
if (query == NULL) {
return;
}
lineno = atoi(query);
if (lineno < 1 || lineno >= editor.nrows) {
editor_set_status("Line number must be between 1 and %d.",
editor.nrows);
free(query);
return;
}
jump_to_position(0, lineno - 1);
free(query);
}
int
cursor_at_eol(void)
{
assert(editor.curx >= 0);
assert(editor.cury >= 0);
assert(editor.cury <= editor.nrows);
assert(editor.curx <= (int)editor.row[editor.cury].size);
return editor.curx == (int)editor.row[editor.cury].size;
}
int
iswordchar(unsigned char c)
{
return isalnum(c) || c == '_' || strchr("/!@#$%^&*+-=~", c) != NULL;
}
void
find_next_word(void)
{
while (cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
}
if (iswordchar(editor.row[editor.cury].b[editor.curx])) {
while (!isspace(editor.row[editor.cury].b[editor.curx]) && !
cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
}
return;
}
if (isspace(editor.row[editor.cury].b[editor.curx])) {
while (isspace(editor.row[editor.cury].b[editor.curx])) {
move_cursor(ARROW_RIGHT, 1);
}
find_next_word();
}
}
void
delete_next_word(void)
{
while (cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
deletech(KILLRING_APPEND);
}
if (iswordchar(editor.row[editor.cury].b[editor.curx])) {
while (!isspace(editor.row[editor.cury].b[editor.curx]) && !
cursor_at_eol()) {
move_cursor(ARROW_RIGHT, 1);
deletech(KILLRING_APPEND);
}
return;
}
if (isspace(editor.row[editor.cury].b[editor.curx])) {
while (isspace(editor.row[editor.cury].b[editor.curx])) {
move_cursor(ARROW_RIGHT, 1);
deletech(KILLRING_APPEND);
}
delete_next_word();
}
}
void
find_prev_word(void)
{
if (editor.cury == 0 && editor.curx == 0) {
return;
}
move_cursor(ARROW_LEFT, 1);
while (cursor_at_eol() || isspace(
editor.row[editor.cury].b[editor.curx])) {
if (editor.cury == 0 && editor.curx == 0) {
return;
}
move_cursor(ARROW_LEFT, 1);
}
while (editor.curx > 0 && !isspace(
editor.row[editor.cury].b[editor.curx - 1])) {
move_cursor(ARROW_LEFT, 1);
}
}
void
delete_prev_word(void)
{
if (editor.cury == 0 && editor.curx == 0) {
return;
}
deletech(KILLRING_PREPEND);
while (editor.cury > 0 || editor.curx > 0) {
if (editor.curx == 0) {
deletech(KILLRING_PREPEND);
continue;
}
if (!isspace(editor.row[editor.cury].b[editor.curx - 1])) {
break;
}
deletech(KILLRING_PREPEND);
}
while (editor.curx > 0) {
if (isspace(editor.row[editor.cury].b[editor.curx - 1])) {
break;
}
deletech(KILLRING_PREPEND);
}
}
void
delete_row(int at)
{
abuf *row = NULL;
if (at < 0 || at >= editor.nrows) {
return;
}
/*
* Update killring with the deleted row's contents followed by a newline
* unless this deletion is an internal merge triggered by deletech at
* start-of-line. In that case, deletech will account for the single
* newline itself and we must NOT also push the entire row here.
*/
if (!editor.no_kill) {
row = &editor.row[at];
/* Start or continue the kill sequence based on editor.killing */
if (row->size > 0) {
/* push raw bytes of the line */
if (!editor.kill) {
killring_start_with_char(
(unsigned char)row->b[0]);
for (int i = 1; i < (int)row->size; i++) {
killring_append_char(
(unsigned char)row->b[i]);
}
} else {
for (int i = 0; i < (int)row->size; i++) {
killring_append_char(
(unsigned char)row->b[i]);
}
}
killring_append_char('\n');
editor.kill = 1;
} else {
if (!editor.kill) {
killring_start_with_char('\n');
} else {
killring_append_char('\n');
}
editor.kill = 1;
}
}
ab_free(&editor.row[at]);
memmove(&editor.row[at],
&editor.row[at + 1],
sizeof(abuf) * (editor.nrows - at - 1));
editor.nrows--;
editor.dirty++;
}
void
row_append_row(abuf *row, char *s, int len)
{
ab_append(row, s, len);
editor.dirty++;
}
void
row_insert_ch(abuf *row, int at, int16_t c)
{
/*
* row_insert_ch just concerns itself with how to update a row.
*/
if ((at < 0) || (at > (int)row->size)) {
at = (int)row->size;
}
assert(c > 0);
ab_resize(row, row->size + 2);
memmove(&row->b[at + 1], &row->b[at], row->size - at + 1);
row->b[at] = c & 0xff;
row->size++;
row->b[row->size] = 0;
}
void
row_delete_ch(abuf *row, int at)
{
if (at < 0 || at >= (int)row->size) {
return;
}
memmove(&row->b[at], &row->b[at + 1], row->size - at);
row->size--;
row->b[row->size] = 0;
editor.dirty++;
}
void
insertch(int16_t c)
{
/*
* insert_ch doesn't need to worry about how to update a
* a row; it can just figure out where the cursor is
* at and what to do.
*/
if (editor.cury == editor.nrows) {
erow_insert(editor.nrows, "", 0);
}
/* Inserting ends kill ring chaining. */
editor.kill = 0;
row_insert_ch(&editor.row[editor.cury],
editor.curx,
(int16_t)(c & 0xff));
editor.curx++;
editor.dirty++;
}
void
deletech(uint8_t op)
{
abuf *row = NULL;
unsigned char dch = 0;
int prev = 0;
if (editor.cury >= editor.nrows) {
return;
}
if (editor.cury == 0 && editor.curx == 0) {
return;
}
row = &editor.row[editor.cury];
if (editor.curx > 0) {
dch = (unsigned char)row->b[editor.curx - 1];
} else {
dch = '\n';
}
if (editor.curx > 0) {
row_delete_ch(row, editor.curx - 1);
editor.curx--;
} else {
editor.curx = editor.row[editor.cury - 1].size;
row_append_row(&editor.row[editor.cury - 1],
row->b,
row->size);
prev = editor.no_kill;
editor.no_kill = 1;
delete_row(editor.cury);
editor.no_kill = prev;
editor.cury--;
}
if (op == KILLRING_FLUSH) {
killring_flush();
editor.kill = 0;
return;
}
if (op == KILLRING_NO_OP) {
return;
}
if (!editor.kill) {
killring_start_with_char(dch);
editor.kill = 1;
return;
}
if (op == KILLING_SET) {
killring_start_with_char(dch);
editor.kill = 1;
} else if (op == KILLRING_APPEND) {
killring_append_char(dch);
} else if (op == KILLRING_PREPEND) {
killring_prepend_char(dch);
}
}
void
open_file(const char *filename)
{
char *line = NULL;
size_t linecap = 0;
ssize_t linelen;
FILE *fp = NULL;
reset_editor();
if (filename == NULL) {
return;
}
editor.filename = strdup(filename);
assert(editor.filename != NULL);
editor.dirty = 0;
if ((fp = fopen(editor.filename, "r")) == NULL) {
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 = NULL;
fclose(fp);
}
/*
* convert our rows to a buffer; caller must free it.
*/
char
*rows_to_buffer(int *buflen)
{
int len = 0;
int j;
char *buf = NULL;
char *p = NULL;
for (j = 0; j < editor.nrows; j++) {
/* extra byte for newline */
len += editor.row[j].size + 1;
}
if (len == 0) {
return NULL;
}
*buflen = len;
buf = malloc(len);
assert(buf != NULL);
p = buf;
for (j = 0; j < editor.nrows; j++) {
memcpy(p, editor.row[j].b, editor.row[j].size);
p += editor.row[j].size;
*p++ = '\n';
}
return buf;
}
int
save_file(void)
{
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 == NULL) {
editor.filename = editor_prompt("Filename: %s", NULL);
if (editor.filename == NULL) {
editor_set_status("Save aborted.");
return 0;
}
}
buf = rows_to_buffer(&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 = NULL;
}
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;
}
uint16_t
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 1;
};
return 0;
}
int16_t
get_keypress(void)
{
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) {
die("get_keypress:read");
}
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;
}
char
*editor_prompt(char *prompt, void (*cb)(char*, int16_t))
{
size_t bufsz = 128;
char *buf = malloc(bufsz);
size_t buflen = 0;
int16_t c;
if (buf == NULL) {
return NULL;
}
buf[0] = '\0';
while (1) {
editor_set_status(prompt, buf);
display_refresh();
while ((c = get_keypress()) <= 0);
if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) {
if (buflen != 0) {
buf[--buflen] = '\0';
}
} else if (c == ESC_KEY || c == CTRL_KEY('g')) {
editor_set_status("");
if (cb) {
cb(buf, c);
}
free(buf);
return NULL;
} else if (c == '\r') {
if (buflen != 0) {
editor_set_status("");
if (cb) {
cb(buf, c);
}
return buf;
}
} else if (c == TAB_KEY) {
/* invoke completion callback without inserting a TAB */
if (cb) {
cb(buf, c);
}
/* keep buflen in sync in case callback edited buf */
buflen = strlen(buf);
} else if (c >= 0x20 && c < 0x7f) {
if (buflen == bufsz - 1) {
bufsz *= 2;
buf = realloc(buf, bufsz);
assert(buf != NULL);
}
buf[buflen++] = (char)(c & 0xff);
buf[buflen] = '\0';
}
if (cb) {
cb(buf, c);
/* keep buflen in sync with any changes the callback made */
buflen = strlen(buf);
}
}
free(buf);
return NULL;
}
void
editor_find_callback(char* query, int16_t c)
{
static int last_match = -1; /* row index of last match */
static int direction = 1; /* 1 = forward, -1 = backward */
static char last_query[128] = {0}; /* remember last successful query */
abuf *row;
int saved_cx = editor.curx;
int saved_cy = editor.cury;
size_t qlen = strlen(query);
char *match;
int i, skip;
if (c == '\r' || c == ESC_KEY || c == CTRL_KEY('g')) {
last_match = -1;
direction = 1;
last_query[0] = '\0';
return;
}
if (c == CTRL_KEY('s') || c == ARROW_DOWN || c == ARROW_RIGHT) {
direction = 1;
} else if (c == CTRL_KEY('r') || c == ARROW_UP || c == ARROW_LEFT) {
direction = -1;
}
if (qlen > 0 && (qlen != strlen(last_query) || strcmp(query, last_query) != 0)) {
last_match = -1;
strcpy(last_query, query);
}
int start_row = editor.cury;
int start_col = editor.curx;
if (last_match == -1) {
if (direction == 1) {
start_col += 1;
}
last_match = editor.cury;
}
int current = last_match - direction;
int wrapped = 0;
for (i = 0; i < editor.nrows; i++) {
current += direction;
if (current >= editor.nrows) {
current = 0;
if (wrapped++) {
break;
}
}
if (current < 0) {
current = editor.nrows - 1;
if (wrapped++) {
break;
}
}
row = &editor.row[current];
char* search_start = row->b;
if (current == start_row && direction == 1 && wrapped == 0) {
skip = start_col;
if (skip > (int)row->size) {
skip = (int)row->size;
}
search_start += skip;
}
match = strnstr(search_start, query, row->size - (search_start - row->b));
if (match) {
last_match = current;
editor.cury = current;
editor.curx = erow_cursor_to_render(row, match - row->b);
if (current == start_row && direction == 1 && last_match == -1) {
editor.curx += start_col; /* adjust if we skipped prefix */
}
scroll();
display_refresh();
return;
}
}
/* No match found */
if (qlen > 0) {
editor_set_status("Failing search: %s", query);
}
editor.curx = saved_cx;
editor.cury = saved_cy;
display_refresh();
}
void
editor_find(void)
{
/* TODO(kyle): consider making this an abuf */
char *query;
int scx = editor.curx;
int scy = editor.cury;
int sco = editor.coloffs;
int sro = editor.rowoffs;
query = editor_prompt("Search (ESC to cancel): %s",
editor_find_callback);
if (query) {
free(query);
query = NULL;
} else {
editor.curx = scx;
editor.cury = scy;
editor.coloffs = sco;
editor.rowoffs = sro;
}
display_refresh();
}
void
editor_openfile(void)
{
char *filename;
/* Add TAB completion for path input */
filename = editor_prompt("Load file: %s", file_open_prompt_cb);
if (filename == NULL) {
return;
}
/* Open into a new buffer */
int nb = buffer_add_empty();
buffer_switch(nb);
open_file(filename);
buffer_save_current();
free(filename);
}
int
first_nonwhitespace(abuf *row)
{
int pos;
wchar_t wc;
mbstate_t state;
size_t len;
if (row == NULL) {
return 0;
}
memset(&state, 0, sizeof(state));
pos = editor.curx;
if (pos > (int)row->size) {
pos = row->size;
}
while (pos < (int)row->size) {
if ((unsigned char)row->b[pos] < 0x80) {
if (!isspace((unsigned char)row->b[pos])) {
return pos;
}
pos++;
continue;
}
len = mbrtowc(&wc, &row->b[pos], row->size - pos, &state);
if (len == (size_t)-1 || len == (size_t)-2) {
break;
}
if (len == 0) {
break;
}
if (!iswspace(wc)) {
break;
}
pos += len;
}
return pos;
}
void
move_cursor_once(int16_t c, int interactive)
{
abuf *row;
int reps = 0;
row = (editor.cury >= editor.nrows) ? NULL : &editor.row[editor.cury];
switch (c) {
case ARROW_UP:
case CTRL_KEY('p'):
if (editor.cury > 0) {
editor.cury--;
row = (editor.cury >= editor.nrows)
? NULL
: &editor.row[editor.cury];
if (interactive) {
editor.curx = first_nonwhitespace(row);
}
}
break;
case ARROW_DOWN:
case CTRL_KEY('n'):
if (editor.cury < editor.nrows - 1) {
editor.cury++;
row = (editor.cury >= editor.nrows)
? NULL
: &editor.row[editor.cury];
if (interactive) {
editor.curx = first_nonwhitespace(row);
}
}
break;
case ARROW_RIGHT:
case CTRL_KEY('f'):
if (!row) {
break;
}
if (editor.curx < (int)row->size) {
editor.curx++;
/* skip over UTF-8 continuation bytes */
while (editor.curx < (int)row->size &&
((unsigned char)row->b[editor.curx] &
0xC0) == 0x80) {
editor.curx++;
}
} else if (editor.curx == (int)row->size && editor.cury < editor.nrows - 1) {
editor.cury++;
editor.curx = 0;
}
break;
case ARROW_LEFT:
case CTRL_KEY('b'):
if (editor.curx > 0) {
editor.curx--;
while (editor.curx > 0 &&
((unsigned char)row->b[editor.curx] &
0xC0) == 0x80) {
editor.curx--;
}
} else if (editor.cury > 0) {
editor.cury--;
editor.curx = editor.row[editor.cury].size;
row = &editor.row[editor.cury];
while (editor.curx > 0 &&
((unsigned char)row->b[editor.curx] &
0xC0) == 0x80) {
editor.curx--;
}
}
break;
case PG_UP:
case PG_DN:
if (c == PG_UP) {
editor.cury = editor.rowoffs;
} else if (c == PG_DN) {
editor.cury = editor.rowoffs + editor.rows - 1;
if (editor.cury > editor.nrows) {
editor.cury = editor.nrows;
}
}
reps = editor.rows;
while (--reps) {
move_cursor(c == PG_UP ? ARROW_UP : ARROW_DOWN, 1);
}
break;
case HOME_KEY:
case CTRL_KEY('a'):
editor.curx = 0;
break;
case END_KEY:
case CTRL_KEY('e'):
if (editor.cury >= editor.nrows) {
break;
}
editor.curx = editor.row[editor.cury].size;
break;
default:
break;
}
}
void
move_cursor(int16_t c, int interactive)
{
int n = uarg_get();
while (n-- > 0) {
move_cursor_once(c, interactive);
}
}
void
newline(void)
{
abuf *row = NULL;
if (editor.cury >= editor.nrows) {
erow_insert(editor.cury, "", 0);
editor.cury++;
editor.curx = 0;
} else if (editor.curx == 0) {
erow_insert(editor.cury, "", 0);
editor.cury++;
editor.curx = 0;
} else {
row = &editor.row[editor.cury];
erow_insert(editor.cury + 1,
&row->b[editor.curx],
row->size - editor.curx);
row = &editor.row[editor.cury];
row->size = editor.curx;
row->b[row->size] = '\0';
editor.cury++;
editor.curx = 0;
}
/* BREAK THE KILL CHAIN \m/ */
editor.kill = 0;
}
void
uarg_start(void)
{
if (editor.uarg == 0) {
editor.ucount = 0;
}else {
if (editor.ucount == 0) {
editor.ucount = 1;
}
editor.ucount *= 4;
}
editor.uarg = 1;
editor_set_status("C-u %d", editor.ucount);
}
void
uarg_digit(int d)
{
if (editor.uarg == 0) {
editor.uarg = 1;
editor.ucount = 0;
}
editor.ucount = editor.ucount * 10 + d;
editor_set_status("C-u %d", editor.ucount);
}
void
uarg_clear(void)
{
editor.uarg = 0;
editor.ucount = 0;
}
int
uarg_get(void)
{
int n = editor.ucount > 0 ? editor.ucount : 1;
uarg_clear();
return n;
}
void
process_kcommand(int16_t c)
{
char *buf = NULL;
int len = 0;
int jumpx = 0;
int jumpy = 0;
int reps = 0;
switch (c) {
case BACKSPACE:
while (editor.curx > 0) {
process_normal(BACKSPACE);
}
break;
case '=':
if (editor.mark_set) {
indent_region();
} else {
editor_set_status("Mark not set.");
}
break;
case '-':
if (editor.mark_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 = editor.mark_curx;
jumpy = editor.mark_cury;
editor.mark_curx = editor.curx;
editor.mark_cury = editor.cury;
jump_to_position(jumpx, jumpy);
editor_set_status("Jumped to mark");
break;
case 'c':
/* Close current buffer (was kill ring clear; that moved to C-k f) */
buffer_close_current();
break;
case 'd':
if (editor.curx == 0 && cursor_at_eol()) {
delete_row(editor.cury);
return;
}
reps = uarg_get();
while (reps--) {
while ((editor.row[editor.cury].size -
editor.curx) > 0) {
process_normal(DEL_KEY);
}
if (reps) {
newline();
}
}
break;
case DEL_KEY:
case CTRL_KEY('d'):
reps = uarg_get();
while (reps--) {
delete_row(editor.cury);
}
break;
case 'e':
case CTRL_KEY('e'):
if (editor.dirty && 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': {
/* Rebound: clear kill ring on C-k f */
len = editor.killring ? (int)editor.killring->size : 0;
killring_flush();
editor_set_status("Kill ring cleared (%d 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 (!editor.mark_set) {
editor_set_status("Mark not set.");
break;
}
jumpx = editor.mark_curx;
jumpy = editor.mark_cury;
editor.mark_curx = editor.curx;
editor.mark_cury = editor.cury;
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(editor.filename);
editor_set_status("Lines of code: %s", buf);
free(buf);
break;
case 'm':
if (system("make") != 0) {
editor_set_status(
"process failed: %s",
strerror(errno));
} else {
editor_set_status("make: ok");
}
break;
case 'q':
if (editor.dirty && 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 (editor.dirty && editor.dirtyex) {
editor_set_status("File not saved - C-k C-r again to reload.");
editor.dirtyex = 0;
return;
}
jumpx = editor.curx;
jumpy = editor.cury;
buf = strdup(editor.filename);
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':
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)
{
int cols = 0;
int 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(int16_t c)
{
int reps = 0;
editor_set_status("hi");
switch (c) {
case '>':
editor.cury = editor.nrows;
editor.curx = 0;
break;
case '<':
editor.cury = 0;
editor.curx = 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 (!editor.mark_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 out of escape-mode */
default:
editor_set_status("unknown ESC key: %04x", c);
}
uarg_clear();
}
int
process_keypress(void)
{
int16_t c = get_keypress();
/* we didn't actually read a key */
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;
}
char
*get_cloc_code_lines(const char *filename)
{
char command[512];
char outbuf[256];
char *result = NULL;
FILE *pipe = NULL;
size_t len = 0;
if (editor.filename == NULL) {
snprintf(command, sizeof(command),
"buffer has no associated file.");
result = malloc((kstrnlen(command, sizeof(command))) + 1);
assert(result != NULL);
strcpy(result, command);
return result;
}
if (editor.dirty) {
snprintf(command, sizeof(command),
"buffer must be saved first.");
result = malloc((kstrnlen(command, sizeof(command))) + 1);
assert(result != NULL);
strcpy(result, command);
return result;
}
snprintf(command,
sizeof(command),
"cloc --quiet %s | tail -2 | head -1 | awk '{print $5}'",
filename);
pipe = popen(command, "r");
if (!pipe) {
snprintf(command, sizeof(command), "Error getting LOC: %s", strerror(errno));
result = (char*)malloc(sizeof(outbuf) + 1);
return NULL;
}
if (fgets(outbuf, sizeof(outbuf), pipe) != NULL) {
len = strlen(outbuf);
if (len > 0 && outbuf[len - 1] == '\n') {
outbuf[len - 1] = '\0';
}
result = malloc(strlen(outbuf) + 1);
assert(result != NULL);
if (result) {
strcpy(result, outbuf);
pclose(pipe);
return result;
}
}
pclose(pipe);
char *zero = malloc(2);
if (zero) {
strcpy(zero, "0");
return zero;
}
return NULL;
}
int
dump_pidfile(void)
{
FILE* pid_file = NULL;
if ((pid_file = fopen("ke.pid", "w")) == NULL) {
editor_set_status("Failed to dump PID file: %s", strerror(errno));
return 0;
}
fprintf(pid_file, "%ld", (long)getpid());
fclose(pid_file);
return 1;
}
/* terminal control moved to term.c */
void
draw_rows(abuf *ab)
{
assert(editor.cols >= 0);
abuf *row;
char buf[editor.cols];
char c;
size_t j;
int len, filerow, padding;
int printed, rx;
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)) {
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 = &editor.row[filerow];
j = 0;
rx = printed = 0;
while (j < row->size && printed < editor.cols) {
c = row->b[j];
if (rx < editor.coloffs) {
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];
int len, rlen;
len = snprintf(status,
sizeof(status),
"%c%cke: %.20s - %d lines",
status_mode_char(),
editor.dirty ? '!' : '-',
editor.filename ? editor.filename : "[no file]",
editor.nrows);
if (editor.mark_set) {
snprintf(mstatus,
sizeof(mstatus),
" | M: %d, %d ",
editor.mark_curx + 1,
editor.mark_cury + 1);
} else {
snprintf(mstatus, sizeof(mstatus), " | M:clear ");
}
rlen = snprintf(rstatus,
sizeof(rstatus),
"L%d/%d C%d %s",
editor.cury + 1,
editor.nrows,
editor.curx + 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)
{
int 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)
{
abuf *row = NULL;
editor.rx = 0;
if (editor.cury < editor.nrows) {
row = &editor.row[editor.cury];
editor.rx = erow_render_to_cursor(row, 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(void)
{
char buf[32];
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 "%d;%dH",
(editor.cury - editor.rowoffs) + 1,
(editor.rx - editor.coloffs) + 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, ab.size);
ab_free(&ab);
}
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);
}
int
kbhit(void)
{
int bytes_waiting;
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.
*
*/
if ((up = process_keypress()) != 0) {
while (kbhit()) {
process_keypress();
}
}
}
}
void
enable_debugging(void)
{
time_t now;
dump_pidfile();
now = time(&now);
printf("time: %s\n", ctime(&now));
fprintf(stderr, "Debug log started %s\n", ctime(&now));
fflush(stderr);
}
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[])
{
int opt;
int debug = 0;
/* argv processing for multiple files and +lineno */
int pending_line = 0; /* line number that applies to next filename */
int first_loaded = 0; /* whether we've opened the first file into initial buffer */
install_signal_handlers();
while ((opt = getopt(argc, argv, "df:")) != -1) {
switch (opt) {
case 'd':
debug = 1;
break;
default:
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();
/* Process remaining argv: accept multiple filenames; a "+N" applies to next filename */
for (int i = 0; i < argc; i++) {
const char *arg = argv[i];
if (arg[0] == '+') {
/* parse line number; if invalid, set to 0 (ignored) */
const char *p = arg + 1;
int v = 0;
if (*p != '\0') {
v = atoi(p);
if (v < 1) v = 0;
}
pending_line = v;
continue;
}
/* It's a filename */
if (!first_loaded) {
/* initial empty buffer already exists; load into it */
open_file(arg);
if (pending_line > 0) {
jump_to_position(0, pending_line - 1);
pending_line = 0;
}
buffer_save_current();
first_loaded = 1;
} else {
/* create and switch to a new buffer for subsequent files */
int nb = buffer_add_empty();
buffer_switch(nb);
open_file(arg);
if (pending_line > 0) {
jump_to_position(0, pending_line - 1);
pending_line = 0;
}
buffer_save_current();
}
}
editor_set_status("C-k q to exit / C-k d to dump core");
display_clear(NULL);
loop();
return 0;
}