Files
ke/editing.c
2025-11-29 14:56:51 -08:00

1575 lines
28 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.
#include <sys/fcntl.h>
#include <sys/syslimits.h>
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wchar.h>
#include "buffer.h"
#include "core.h"
#include "editor.h"
#include "editing.h"
#include "killring.h"
#include "term.h"
/*
* TODO(kyle): not proud of this, but it does work. It needs to be
* cleaned up and the number of buffers consolidated.
*/
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);
}
/* copy to input buffer (max 127 chars + NUL) avoiding truncation warnings */
do {
const char *src__ = newbuf[0] ? newbuf : names[0];
size_t cap__ = 128u;
size_t len__ = strnlen(src__, cap__ - 1);
memcpy(buf, src__, len__);
buf[len__] = '\0';
} while (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);
}
/* copy to input buffer (max 127 chars + NUL) avoiding truncation warnings */
do {
const char *src__ = newbuf;
size_t cap__ = 128u;
size_t len__ = strnlen(src__, cap__ - 1);
memcpy(buf, src__, len__);
buf[len__] = '\0';
} while (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 ((size_t) at < ENROWS) {
memmove(&EROW[at + 1], &EROW[at],
sizeof(abuf) * (ENROWS - (size_t) at));
}
ab_init(&EROW[at]);
ab_append(&EROW[at], s, (size_t) len);
ENROWS++;
}
void
jump_to_position(size_t col, size_t row)
{
if (ENROWS <= 0) {
ECURX = 0;
ECURY = 0;
display_refresh();
return;
}
if (row >= ENROWS) {
row = ENROWS - 1;
}
if (col > EROW[row].size) {
col = EROW[row].size;
}
ECURX = col;
ECURY = row;
display_refresh();
}
void
goto_line(void)
{
size_t lineno = 0;
char *query = editor_prompt("Line: %s", NULL);
if (query == NULL) {
return;
}
lineno = strtoul(query, NULL, 10);
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(ECURY <= ENROWS);
assert(ECURX <= EROW[ECURY].size);
return ECURX == 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 (ECURY == 0 && ECURX == 0) {
return;
}
move_cursor(ARROW_LEFT, 1);
while (cursor_at_eol() || isspace(EROW[ECURY].b[ECURX])) {
if (ECURY == 0 && ECURX == 0) {
return;
}
move_cursor(ARROW_LEFT, 1);
}
while (ECURX > 0 && !isspace(EROW[ECURY].b[ECURX - 1])) {
move_cursor(ARROW_LEFT, 1);
}
}
void
delete_prev_word(void)
{
if (ECURY == 0 && ECURX == 0) {
return;
}
deletech(KILLRING_PREPEND);
while (ECURY > 0 || ECURX > 0) {
if (ECURX == 0) {
deletech(KILLRING_PREPEND);
continue;
}
if (!isspace(EROW[ECURY].b[ECURX - 1])) {
break;
}
deletech(KILLRING_PREPEND);
}
while (ECURX > 0) {
if (isspace(EROW[ECURY].b[ECURX - 1])) {
break;
}
deletech(KILLRING_PREPEND);
}
}
void
delete_row(const size_t at)
{
abuf *row = NULL;
if (at >= ENROWS) {
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 = &EROW[at];
if (row->size > 0) {
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(&EROW[at]);
memmove(&EROW[at],
&EROW[at + 1],
sizeof(abuf) * (ENROWS - at - 1));
ENROWS--;
EDIRTY++;
}
void
row_append_row(abuf *row, const char *s, const int len)
{
ab_append(row, s, len);
EDIRTY++;
}
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;
EDIRTY++;
}
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 (ECURY == ENROWS) {
erow_insert(ENROWS, "", 0);
}
/* Inserting ends kill ring chaining. */
editor.kill = 0;
row_insert_ch(&EROW[ECURY],
ECURX,
(int16_t) (c & 0xff));
ECURX++;
EDIRTY++;
}
void
deletech(uint8_t op)
{
abuf *row = NULL;
unsigned char dch = 0;
int prev = 0;
if (ECURY >= ENROWS) {
return;
}
if (ECURY == 0 && ECURX == 0) {
return;
}
row = &EROW[ECURY];
if (ECURX > 0) {
dch = (unsigned char) row->b[ECURX - 1];
} else {
dch = '\n';
}
if (ECURX > 0) {
row_delete_ch(row, ECURX - 1);
ECURX--;
} else {
ECURX = (int) EROW[ECURY - 1].size;
row_append_row(&EROW[ECURY - 1],
row->b,
(int) row->size);
prev = editor.no_kill;
editor.no_kill = 1;
delete_row(ECURY);
editor.no_kill = prev;
ECURY--;
}
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;
size_t i = 0;
FILE *fp = NULL;
buffer *cur = NULL;
cur = buffer_current();
if (cur == NULL) {
return;
}
/* Clear editor working set and the current buffers contents so we load
* fresh data instead of appending to any previous rows. */
reset_editor();
if (EROW != NULL && ENROWS > 0) {
for (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);
}
/*
* convert our rows to a buffer; caller must free it.
*/
char
*rows_to_buffer(int *buflen)
{
size_t len = 0;
size_t j = 0;
char *buf = NULL;
char *p = NULL;
for (j = 0; j < ENROWS; j++) {
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 (!EDIRTY) {
editor_set_status("No changes to save.");
return 0;
}
if (EFILENAME == NULL) {
EFILENAME = editor_prompt("Filename: %s", NULL);
if (EFILENAME == NULL) {
editor_set_status("Save aborted.");
return 0;
}
}
buf = rows_to_buffer(&len);
fd = open(EFILENAME, 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", EFILENAME, buf);
} else {
editor_set_status("Wrote %d bytes to %s.", len, EFILENAME);
EDIRTY = 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 ssize_t 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 = ECURX;
const int saved_cy = ECURY;
const size_t qlen = strlen(query);
const char *hay = NULL; /* the haystack, so to speak */
const char *match = NULL;
size_t i = 0;
size_t limit = 0;
size_t skip = 0;
size_t haylen = 0;
size_t start_row = ECURY;
size_t start_col = ECURX;
size_t wrapped = 0;
ssize_t current = 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;
/* copy query safely into last_query */
strncpy(last_query, query, sizeof(last_query) - 1);
last_query[sizeof(last_query) - 1] = '\0';
}
if (last_match == -1) {
last_match = ECURY;
}
current = last_match - direction;
wrapped = 0;
for (i = 0; i < ENROWS; i++) {
current += direction;
if ((size_t) current >= ENROWS) {
current = 0;
if (wrapped++) {
break;
}
}
if (current < 0) {
current = ENROWS - 1;
if (wrapped++) {
break;
}
}
row = &EROW[current];
/* Determine match based on direction. For forward searches, start just
* after the current cursor when on the same row. For backward searches,
* find the last occurrence before the current cursor when on the same row,
* and in other rows find the last occurrence in the row. */
if (direction == 1) {
hay = row->b;
haylen = row->size;
if ((size_t) current == start_row && wrapped == 0) {
/* start just after the current position to avoid re-finding same match */
skip = start_col + 1;
if (skip > haylen) {
skip = haylen;
}
hay += skip;
haylen -= (size_t) skip;
}
match = (qlen > 0) ?
strnstr(hay, query, haylen) : NULL;
} else {
limit = row->size;
if ((size_t) current == start_row && wrapped == 0) {
/* Only consider text strictly before the cursor */
if ((size_t) start_col < limit) limit = (size_t) start_col;
}
if (qlen > 0 && limit >= qlen) {
const char *p = row->b;
const char *last = NULL;
size_t remaining = limit;
while (remaining >= qlen) {
const char *q = strnstr(p, query, remaining);
if (!q) break;
last = q;
/* move one byte forward to look for later occurrences */
size_t advance = (size_t) (q - p) + 1;
p += advance;
remaining = limit - (size_t) (p - row->b);
}
match = last;
} else {
match = NULL;
}
}
if (match) {
last_match = current;
ECURY = current;
/* ECURX stores the raw byte index into the row buffer. */
ECURX = (int) (match - row->b);
scroll();
display_refresh();
return;
}
}
if (qlen > 0) {
editor_set_status("Failing search: %s", query);
}
ECURX = saved_cx;
ECURY = saved_cy;
display_refresh();
}
void
editor_find(void)
{
/* TODO(kyle): consider making this an abuf */
char *query = NULL;
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 = NULL;
const buffer *cur = NULL;
int nb = 0;
filename = editor_prompt("Load file: %s", file_open_prompt_cb);
if (filename == NULL) {
return;
}
cur = buffer_current();
if (editor.bufcount == 1 && buffer_is_unnamed_and_empty(cur)) {
open_file(filename);
} else {
nb = buffer_add_empty();
buffer_switch(nb);
open_file(filename);
}
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 = ECURX;
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 = (ECURY >= ENROWS) ? NULL : &EROW[ECURY];
switch (c) {
case ARROW_UP:
case CTRL_KEY('p'):
if (ECURY > 0) {
ECURY--;
row = (ECURY >= ENROWS) ? NULL : &EROW[ECURY];
if (interactive) {
ECURX = first_nonwhitespace(row);
} else if (row) {
if (ECURX > row->size) {
ECURX = row->size;
}
}
}
break;
case ARROW_DOWN:
case CTRL_KEY('n'):
if (ECURY < ENROWS - 1) {
ECURY++;
row = (ECURY >= ENROWS) ? NULL : &EROW[ECURY];
if (interactive) {
ECURX = first_nonwhitespace(row);
} else if (row) {
if (ECURX > row->size) {
ECURX = row->size;
}
}
}
break;
case ARROW_RIGHT:
case CTRL_KEY('f'):
if (!row) {
break;
}
if (ECURX < row->size) {
ECURX++;
/* skip over UTF-8 continuation bytes */
while (ECURX < row->size &&
((unsigned char) row->b[ECURX] &
0xC0) == 0x80) {
ECURX++;
}
} else if (ECURX == row->size && ECURY < ENROWS - 1) {
ECURY++;
ECURX = 0;
}
break;
case ARROW_LEFT:
case CTRL_KEY('b'):
if (ECURX > 0) {
ECURX--;
while (ECURX > 0 &&
((unsigned char) row->b[ECURX] &
0xC0) == 0x80) {
ECURX--;
}
} else if (ECURY > 0) {
ECURY--;
ECURX = (int) EROW[ECURY].size;
row = &EROW[ECURY];
while (ECURX > 0 &&
((unsigned char) row->b[ECURX] &
0xC0) == 0x80) {
ECURX--;
}
}
break;
case PG_UP:
case PG_DN:
if (c == PG_UP) {
ECURY = EROWOFFS;
} else if (c == PG_DN) {
ECURY = EROWOFFS + editor.rows - 1;
if (ECURY > ENROWS) {
ECURY = ENROWS;
}
}
reps = editor.rows;
while (--reps) {
move_cursor(c == PG_UP ? ARROW_UP : ARROW_DOWN, 1);
}
break;
case HOME_KEY:
case CTRL_KEY('a'):
ECURX = 0;
break;
case END_KEY:
case CTRL_KEY('e'):
if (ECURY >= ENROWS) {
break;
}
ECURX = (int) EROW[ECURY].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
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
newline(void)
{
size_t rhs_len = 0;
char *tmp = NULL;
if (ECURY >= ENROWS) {
erow_insert(ECURY, "", 0);
ECURY++;
ECURX = 0;
} else if (ECURX == 0) {
erow_insert(ECURY, "", 0);
ECURY++;
ECURX = 0;
} else {
/*
* IMPORTANT: Do not keep a pointer to EROW[ECURY] across erow_insert(),
* as erow_insert() may realloc the rows array and invalidate it.
*/
rhs_len = EROW[ECURY].size - (size_t)ECURX;
if (rhs_len > 0) {
tmp = malloc(rhs_len);
assert(tmp != NULL);
memcpy(tmp, &EROW[ECURY].b[ECURX], rhs_len);
}
/* Insert the right-hand side as a new row first (may realloc rows). */
erow_insert(ECURY + 1, tmp ? tmp : "", (int)rhs_len);
if (tmp) {
free(tmp);
}
/* Now safely shrink the original row (re-fetch by index). */
EROW[ECURY].size = ECURX;
if (EROW[ECURY].cap <= EROW[ECURY].size) {
ab_resize(&EROW[ECURY], EROW[ECURY].size + 1);
}
EROW[ECURY].b[EROW[ECURY].size] = '\0';
ECURY++;
ECURX = 0;
}
editor.kill = 0; /* BREAK THE KILL CHAIN \m/ */
EDIRTY++;
}
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 (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 (EDIRTY) {
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));
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);
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;
}