2874 lines
51 KiB
C
2874 lines
51 KiB
C
/*
|
||
* 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 "editor.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 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 */
|
||
|
||
|
||
/* 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 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(const 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);
|
||
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 loop(void);
|
||
void enable_debugging(void);
|
||
void deathknell(void);
|
||
static void signal_handler(int sig);
|
||
static void install_signal_handlers(void);
|
||
|
||
|
||
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)
|
||
{
|
||
size_t i = 0;
|
||
|
||
if (!a || !b) {
|
||
return 0;
|
||
}
|
||
|
||
while (a[i] && b[i] && a[i] == b[i]) {
|
||
i++;
|
||
}
|
||
|
||
return i;
|
||
}
|
||
|
||
|
||
/*
|
||
* TODO(kyle): not proud of this, but it does work. It needs to be
|
||
* cleaned up and the number of buffers consolidated.
|
||
*/
|
||
static void
|
||
file_open_prompt_cb(char *buf, const int16_t key)
|
||
{
|
||
DIR *dirp = NULL;
|
||
const char *name = NULL;
|
||
const char *names[128] = {0};
|
||
char ext[256] = {0};
|
||
char full[PATH_MAX] = {0};
|
||
char msg[80] = {0};
|
||
char newbuf[PATH_MAX] = {0};
|
||
int isdir[128] = {0};
|
||
struct dirent *de = NULL;
|
||
const char *slash = NULL;
|
||
char dirpath[PATH_MAX] = {0};
|
||
char base[256] = {0};
|
||
int n = 0;
|
||
size_t cur = 0;
|
||
size_t k = 0;
|
||
size_t lcp = 0;
|
||
size_t to_copy = 0;
|
||
size_t dlen = 0;
|
||
size_t plen = 0;
|
||
|
||
if (key != TAB_KEY) {
|
||
return;
|
||
}
|
||
|
||
slash = strrchr(buf, '/');
|
||
if (slash) {
|
||
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';
|
||
}
|
||
|
||
dirp = opendir(dirpath);
|
||
if (!dirp) {
|
||
editor_set_status("No such dir: %s", dirpath);
|
||
return;
|
||
}
|
||
|
||
plen = strlen(base);
|
||
while ((de = readdir(dirp)) != NULL) {
|
||
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 */
|
||
if (snprintf(full, sizeof(full),
|
||
"%s/%s", dirpath, name) >= 0) {
|
||
isdir[n] = path_is_dir(full);
|
||
} else {
|
||
isdir[n] = 0;
|
||
}
|
||
|
||
n++;
|
||
}
|
||
}
|
||
}
|
||
|
||
closedir(dirp);
|
||
|
||
if (n == 0) {
|
||
editor_set_status("No file matches '%s' in %s", base, dirpath);
|
||
return;
|
||
}
|
||
|
||
lcp = strlen(names[0]);
|
||
for (int i = 1; i < n; i++) {
|
||
k = str_lcp2(names[0], names[i]);
|
||
if (k < lcp) {
|
||
lcp = k;
|
||
}
|
||
|
||
if (lcp == 0) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
newbuf[0] = '\0';
|
||
if (slash) {
|
||
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);
|
||
}
|
||
|
||
/* appending: 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);
|
||
}
|
||
|
||
strncpy(buf, newbuf[0] ? newbuf : names[0], 127);
|
||
buf[127] = '\0';
|
||
editor_set_status("Unique match: %s%s", names[0], isdir[0] ? "/" : "");
|
||
} else {
|
||
cur = strlen(base);
|
||
if (lcp > cur) {
|
||
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';
|
||
|
||
strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1);
|
||
strncat(newbuf, ext, sizeof(newbuf) - strlen(newbuf) - 1);
|
||
} else {
|
||
strncat(newbuf, base, sizeof(newbuf) - strlen(newbuf) - 1);
|
||
}
|
||
|
||
strncpy(buf, newbuf, 127);
|
||
buf[127] = '\0';
|
||
|
||
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]);
|
||
}
|
||
}
|
||
|
||
|
||
int
|
||
erow_render_to_cursor(const abuf *row, const 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(EROW, sizeof(abuf) * (ENROWS + 1));
|
||
|
||
assert(row != NULL);
|
||
EROW = row;
|
||
|
||
if (at < ENROWS) {
|
||
memmove(&EROW[at + 1], &EROW[at],
|
||
sizeof(abuf) * (ENROWS - at));
|
||
}
|
||
|
||
ab_init(&EROW[at]);
|
||
ab_append(&EROW[at], s, len);
|
||
ENROWS++;
|
||
}
|
||
|
||
|
||
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)
|
||
{
|
||
int count = 0;
|
||
int curx = ECURX;
|
||
int cury = ECURY;
|
||
|
||
int markx = EMARK_CURX;
|
||
int marky = EMARK_CURY;
|
||
|
||
if (!cursor_after_mark()) {
|
||
swap_int(&curx, &markx);
|
||
swap_int(&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)
|
||
{
|
||
int curx = ECURX;
|
||
int cury = ECURY;
|
||
int markx = EMARK_CURX;
|
||
int marky = EMARK_CURY;
|
||
|
||
if (!EMARK_SET) {
|
||
return;
|
||
}
|
||
|
||
/* kill the current killring first */
|
||
killring_flush();
|
||
|
||
if (!cursor_after_mark()) {
|
||
swap_int(&curx, &markx);
|
||
swap_int(&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)
|
||
{
|
||
int start_row = 0;
|
||
int end_row = 0;
|
||
int 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 (start_row < 0) {
|
||
start_row = 0;
|
||
}
|
||
|
||
if (end_row >= ENROWS) {
|
||
end_row = ENROWS - 1;
|
||
}
|
||
|
||
if (start_row >= ENROWS || end_row < 0) {
|
||
return;
|
||
}
|
||
|
||
for (i = start_row; i <= end_row; i++) {
|
||
row_insert_ch(&EROW[i], 0, '\t');
|
||
}
|
||
|
||
ECURX = 0;
|
||
EDIRTY++;
|
||
}
|
||
|
||
|
||
void
|
||
unindent_region(void)
|
||
{
|
||
int start_row = 0;
|
||
int end_row = 0;
|
||
int i = 0;
|
||
int 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 >= 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 (row >= 0 && row < ENROWS && col > (int) EROW[row].size) {
|
||
col = (int) EROW[row].size;
|
||
}
|
||
|
||
ECURX = col;
|
||
ECURY = 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 >= ENROWS) {
|
||
editor_set_status("Line number must be between 1 and %d.",
|
||
ENROWS);
|
||
free(query);
|
||
return;
|
||
}
|
||
|
||
jump_to_position(0, lineno - 1);
|
||
free(query);
|
||
}
|
||
|
||
|
||
int
|
||
cursor_at_eol(void)
|
||
{
|
||
assert(ECURX >= 0);
|
||
assert(ECURY >= 0);
|
||
assert(ECURY <= ENROWS);
|
||
assert(ECURX <= (int)EROW[ECURY].size);
|
||
|
||
return ECURX == (int)EROW[ECURY].size;
|
||
}
|
||
|
||
|
||
int
|
||
iswordchar(const 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(EROW[ECURY].b[ECURX])) {
|
||
while (!isspace(EROW[ECURY].b[ECURX]) && !
|
||
cursor_at_eol()) {
|
||
move_cursor(ARROW_RIGHT, 1);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (isspace(EROW[ECURY].b[ECURX])) {
|
||
while (isspace(EROW[ECURY].b[ECURX])) {
|
||
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(EROW[ECURY].b[ECURX])) {
|
||
while (!isspace(EROW[ECURY].b[ECURX]) && !
|
||
cursor_at_eol()) {
|
||
move_cursor(ARROW_RIGHT, 1);
|
||
deletech(KILLRING_APPEND);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (isspace(EROW[ECURY].b[ECURX])) {
|
||
while (isspace(EROW[ECURY].b[ECURX])) {
|
||
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(const 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, const char *s, const int len)
|
||
{
|
||
ab_append(row, s, len);
|
||
editor.dirty++;
|
||
}
|
||
|
||
|
||
void
|
||
row_insert_ch(abuf *row, int at, const 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, const 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(const 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 = 0;
|
||
FILE *fp = NULL;
|
||
buffer *cur = NULL;
|
||
|
||
/* Ensure we have a current buffer to load into */
|
||
cur = buffer_current();
|
||
if (cur == NULL) {
|
||
return;
|
||
}
|
||
|
||
/* Clear editor working set and the current buffer’s contents so we load
|
||
* fresh data instead of appending to any previous rows. */
|
||
reset_editor();
|
||
|
||
if (EROW != NULL && ENROWS > 0) {
|
||
for (int i = 0; i < ENROWS; i++) {
|
||
ab_free(&EROW[i]);
|
||
}
|
||
free(EROW);
|
||
EROW = NULL;
|
||
ENROWS = 0;
|
||
}
|
||
|
||
/* Reset cursor/scroll positions for the buffer */
|
||
ECURX = ECURY = 0;
|
||
ERX = 0;
|
||
EROWOFFS = ECOLOFFS = 0;
|
||
|
||
if (filename == NULL) {
|
||
return;
|
||
}
|
||
|
||
EFILENAME = strdup(filename);
|
||
assert(EFILENAME != NULL);
|
||
|
||
EDIRTY = 0;
|
||
fp = fopen(EFILENAME, "r");
|
||
if (fp == 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(ENROWS, line, (int)linelen);
|
||
}
|
||
}
|
||
|
||
free(line);
|
||
line = NULL;
|
||
fclose(fp);
|
||
|
||
/* sync changes back */
|
||
editor.row = EROW;
|
||
editor.nrows = ENROWS;
|
||
editor.filename = EFILENAME;
|
||
editor.dirty = EDIRTY;
|
||
}
|
||
|
||
|
||
/*
|
||
* convert our rows to a buffer; caller must free it.
|
||
*/
|
||
char
|
||
*rows_to_buffer(int *buflen)
|
||
{
|
||
int len = 0;
|
||
int j = 0;
|
||
char *buf = NULL;
|
||
char *p = NULL;
|
||
|
||
for (j = 0; j < ENROWS; j++) {
|
||
/* extra byte for newline */
|
||
len += EROW[j].size + 1;
|
||
}
|
||
|
||
if (len == 0) {
|
||
return NULL;
|
||
}
|
||
|
||
*buflen = len;
|
||
buf = malloc(len);
|
||
assert(buf != NULL);
|
||
p = buf;
|
||
|
||
for (j = 0; j < ENROWS; j++) {
|
||
memcpy(p, EROW[j].b, EROW[j].size);
|
||
p += EROW[j].size;
|
||
*p++ = '\n';
|
||
}
|
||
|
||
return buf;
|
||
}
|
||
|
||
|
||
int
|
||
save_file(void)
|
||
{
|
||
int fd = -1;
|
||
int len = 0;
|
||
int status = 1;
|
||
char *buf = NULL;
|
||
|
||
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);
|
||
fd = open(editor.filename, O_RDWR | O_CREAT, 0644);
|
||
if (fd == -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(const 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;
|
||
default:
|
||
return 0;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
int16_t
|
||
get_keypress(void)
|
||
{
|
||
char seq[3] = {0};
|
||
unsigned char uc = 0;
|
||
int16_t c = 0;
|
||
|
||
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;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
} 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:
|
||
break;
|
||
}
|
||
}
|
||
} else if (seq[0] == 'O') {
|
||
switch (seq[1]) {
|
||
case 'F':
|
||
return END_KEY;
|
||
case 'H':
|
||
return HOME_KEY;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0x1b;
|
||
}
|
||
|
||
return c;
|
||
}
|
||
|
||
|
||
char
|
||
*editor_prompt(const 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 = back */
|
||
static char last_query[128] = {0}; /* last successful query */
|
||
abuf *row = NULL;
|
||
const int saved_cx = editor.curx;
|
||
const int saved_cy = editor.cury;
|
||
const size_t qlen = strlen(query);
|
||
const char *match = NULL;
|
||
const char *search_start = NULL;
|
||
int i = 0;
|
||
int skip = 0;
|
||
int start_row = 0;
|
||
int start_col = 0;
|
||
|
||
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);
|
||
}
|
||
|
||
start_row = editor.cury;
|
||
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];
|
||
|
||
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 = ECURX;
|
||
int scy = ECURY;
|
||
int sco = ECOLOFFS;
|
||
int sro = EROWOFFS;
|
||
|
||
query = editor_prompt("Search (ESC to cancel): %s",
|
||
editor_find_callback);
|
||
if (query) {
|
||
free(query);
|
||
query = NULL;
|
||
} else {
|
||
ECURX = scx;
|
||
ECURY = scy;
|
||
ECOLOFFS = sco;
|
||
EROWOFFS = 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;
|
||
}
|
||
|
||
/* If the only buffer is an unnamed, empty buffer, reuse it */
|
||
buffer *cur = buffer_current();
|
||
if (editor.bufcount == 1 && buffer_is_unnamed_and_empty(cur)) {
|
||
open_file(filename);
|
||
buffer_save_current();
|
||
} else {
|
||
/* 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(const int16_t c, int interactive)
|
||
{
|
||
abuf *row = NULL;
|
||
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 = (int)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 = (int)editor.row[editor.cury].size;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
|
||
void
|
||
move_cursor(const int16_t c, const 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(const 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 ((EROW[ECURY].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':
|
||
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)
|
||
{
|
||
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(const 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)
|
||
{
|
||
const 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] = {0};
|
||
char outbuf[256] = {0};
|
||
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;
|
||
}
|
||
|
||
|
||
void
|
||
draw_rows(abuf *ab)
|
||
{
|
||
assert(editor.cols >= 0);
|
||
|
||
abuf *row = NULL;
|
||
char buf[editor.cols];
|
||
char c = 0;
|
||
size_t j = 0;
|
||
int len = 0;
|
||
int filerow = 0;
|
||
int padding = 0;
|
||
int printed = 0;
|
||
int rx = 0;
|
||
int y = 0;
|
||
|
||
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 = &EROW[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 = 0;
|
||
int rlen = 0;
|
||
|
||
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 = (int)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;
|
||
|
||
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] = {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 "%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, (int)ab.size);
|
||
ab_free(&ab);
|
||
}
|
||
|
||
|
||
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;
|
||
}
|
||
|
||
buffer_save_current();
|
||
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;
|
||
}
|
||
|
||
buffer_save_current();
|
||
}
|
||
}
|
||
|
||
editor_set_status("C-k q to exit / C-k d to dump core");
|
||
|
||
display_clear(NULL);
|
||
loop();
|
||
|
||
return 0;
|
||
}
|