#include "editline.h"
#include "error.h"
#include "exproriented.h"
#include "mem.h"
#include "term.h"
#include "testing.h"
#include <ctype.h>
#define getchar (char)getchar
#define is_space (bool)isspace
#define is_alnum (bool)isalnum
mut_cursor_t movecur(int n, mut_cursor_t cur) {
size_t len = cur.base->len;
size_t pos = cur.pos;
cur.pos = n > (int)(len - pos) ? len : -n > (int)pos ? 0 : pos + (size_t)n;
return cur;
}
test (movecur) {
mut_slice_t s dropslice = mutFromString("sample text.");
mut_cursor_t ms = FROM_SLICE(s);
ms = movecur(1, ms);
expecteq(1, ms.pos);
ms = movecur(3, ms);
expecteq(4, ms.pos);
ms = movecur(-4, ms);
expecteq(0, ms.pos);
ms = movecur(-1, ms);
expecteq(0, ms.pos);
ms = movecur(999, ms);
expecteq(ms.base->len, ms.pos);
}
mut_cursor_t findmove(char ch, int dir, mut_cursor_t c) {
size_t new
= (size_t)((dir > 0) ? memchr(c.base->s + c.pos, ch, c.base->len - c.pos)
: memrchr(c.base->s, ch, c.pos))
?: $return(c);
return (mut_cursor_t){
.base = c.base,
.pos = new - (size_t)c.base->s,
};
}
test (findmove) {
mut_slice_t s = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
#define CASE(ch, d) \
c = findmove(ch, d, c); \
expecteq(ch, c.base->s[c.pos]);
MAP_PAIR(CASE, (' ', 1), ('e', 1), ('s', -1), ('.', 1))
size_t prev = c.pos;
c = findmove('z', -1, c);
expecteq(prev, c.pos);
}
mut_cursor_t fw(mut_cursor_t c) {
char const *s = c.base->s + c.pos;
if (is_space(*s)) return movecur((int)skipByte(s, ' '), c);
for (bool was_alnum = is_alnum(*s);
!is_space(*s) && s < c.base->s + c.base->len
&& (is_alnum(*s) == was_alnum);
s++);
s += skipByte(s, ' ');
return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}
test (fw) {
mut_slice_t s dropslice = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
c = fw(c);
expecteq(7, c.pos);
c = fw(c);
expecteq(11, c.pos);
}
mut_cursor_t bw(mut_cursor_t c) {
char const *s = c.base->s + c.pos - 1;
for (; is_space(*s); s--);
for (bool was_alnum = is_alnum(*s);
!is_space(s[-1]) && c.base->s < s && (is_alnum(s[-1]) == was_alnum);
s--);
return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}
test (bw) {
mut_slice_t s dropslice = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
c.pos = s.len;
c = bw(c);
expecteq(11, c.pos);
c = bw(c);
expecteq(7, c.pos);
}
mut_cursor_t fW(mut_cursor_t c) {
char const *s = memchr(c.base->s + c.pos, ' ', c.base->len - c.pos)
?: c.base->s + c.base->len;
s += skipByte(s, ' ');
return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}
test (fW) {
mut_slice_t s dropslice = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
c = fW(c);
expecteq(7, c.pos);
c = fW(c);
expecteq(12, c.pos);
}
mut_cursor_t bW(mut_cursor_t c) {
char const *s = c.base->s + c.pos - 1;
for (; is_space(*s) && c.base->s < s; s--);
s = memrchr(c.base->s, ' ', (size_t)(s - c.base->s)) ?: c.base->s - 1;
return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s + 1)};
}
test (bW) {
mut_slice_t s dropslice = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
c.pos = s.len;
c = bW(c);
expecteq(7, c.pos);
c = bW(c);
expecteq(0, c.pos);
}
void deletes(mut_slice_t *ms, range_t range) {
memmove(ms->s + range.begin, ms->s + range.end, ms->len - range.end);
ms->len -= range.end - range.begin;
}
test (deletes) {
mut_slice_t s dropslice = mutFromString("sample text.");
deletes(&s, (range_t){0, 7});
expecteq(mutFromString("text."), s);
}
static void handleEs(char key, mut_cursor_t *c) {
switch (key) {
case '3': if (getchar() != '~') break;
if (c->pos == c->base->len) break;
char *s = c->base->s + c->pos;
memmove(s, s + 1, c->base->len - c->pos - 1);
c->base->len--;
break;
case 'C': *c = movecur(1, *c);
break;
case 'D': *c = movecur(-1, *c);
break;
case 'F': c->pos = c->base->len;
break;
case 'H': c->pos = 0;
break;
default:
DISPERR("unknown escape sequence: ", key);
}
}
static range_t handleTxtObj(char txtobj, mut_cursor_t c) {
switch (txtobj) {
case 'w':
return (range_t){.begin = fw(c).pos, .end = bw(c).pos};
case 'W':
return (range_t){.begin = fW(c).pos, .end = bW(c).pos};
case 'b':
return (range_t){
.begin = findmove(')', 1, c).pos,
.end = findmove('(', -1, c).pos,
};
case ']':
return (range_t){
.begin = findmove(']', 1, c).pos,
.end = findmove('[', -1, c).pos,
};
default:
return (range_t){};
}
}
void inserts(slice_t s, mut_cursor_t *c) {
size_t len = s.len + c->base->len;
size_t cap = stdc_bit_ceil(len);
if (c->base->cap < len) {
c->base->s = realloc(c->base->s, cap);
c->base->cap = cap;
}
char *str = c->base->s + c->pos;
memmove(str + s.len, str, c->base->len - c->pos);
memcpy(str, s.s, s.len);
c->pos += s.len;
c->base->len += s.len;
}
test (inserts) {
auto s = mutFromString("sample text.");
mut_cursor_t c = FROM_SLICE(s);
c.pos = s.len - 1;
inserts(fromString("new string"), &c);
expecteq(mutFromString("sample textnew string."), *c.base);
c.pos = 11;
inserts(fromString("extra string"), &c);
expecteq(mutFromString("sample textextra stringnew string."), *c.base);
}
static void insbind(char ch, mut_cursor_t *c) {
switch (ch) {
case '(': inserts(fromString("()"), c);
c->pos--;
break;
case ')': {
char const *pos = strchr(c->base->s + c->pos, ')') ?: p$(goto dflt);
c->pos = (size_t)(pos - c->base->s + 1);
} break;
case '[': inserts(fromString("[]"), c);
c->pos--;
break;
case ']': {
char *pos = strchr(c->base->s + c->pos, ']') ?: p$(goto dflt);
c->pos = (size_t)(pos - c->base->s + 1);
} break;
dflt:
default:
inserts((slice_t){.s = &ch, .len = 1}, c);
}
}
auto handle_printable = insbind;
static void nrmbind(char ch, mut_cursor_t *c) {
switch (ch) {
case 'h':
*c = movecur(-1, *c);
break;
case 'l':
*c = movecur(1, *c);
break;
case 'w':
*c = fw(*c);
break;
case 'b':
*c = bw(*c);
break;
case 'W':
*c = fW(*c);
break;
case 'B':
*c = bW(*c);
break;
case 'f':
*c = findmove(getchar(), 1, *c);
break;
case 'F':
*c = findmove(getchar(), -1, *c);
break;
case 'A':
c->pos = c->base->len;
[[fallthrough]];
case 'a':
c->pos += c->pos != c->base->len;
handle_printable = insbind;
break;
case 'I':
c->pos = 0;
[[fallthrough]];
case 'i':
handle_printable = insbind;
break;
case 'c':
handle_printable = insbind;
[[fallthrough]];
case 'd': {
size_t orig_pos = c->pos;
char input = getchar();
range_t range
= $if(input == 'i' || input == 'a')({ handleTxtObj(getchar(), *c); })
$else({
nrmbind(input, c);
(range_t){
.begin = less(c->pos, orig_pos),
.end = more(c->pos, orig_pos),
};
});
if (range.begin == range.end) break;
deletes(c->base, range);
c->pos = range.begin;
} break;
case 'C':
handle_printable = insbind;
[[fallthrough]];
case 'D':
c->base->len = c->pos;
break;
case 'r':
c->base->s[c->pos] = getchar();
break;
case '[':
handleEs(getchar(), c);
handle_printable = insbind;
break;
default:
DISPERR("unknown char: ", ch);
}
}
bool editline(size_t sz, char *buf) {
mut_slice_t s = mutFromString("");
mut_cursor_t c = FROM_SLICE(s);
struct termios __ ondrop(disableRawMode) = enableRawMode();
char ch;
while (ch = getchar(), ch != ctrld && ch != '\n' && c.base->len < sz - 1) {
switch (ch) {
case es:
handle_printable = nrmbind;
break;
case backspace:
if (c.pos == 0) continue;
memmove(c.base->s + c.pos - 1, c.base->s + c.pos, c.base->len - c.pos);
c.pos--;
c.base->len--;
break;
default:
handle_printable(ch, &c);
break;
}
PRINT(ESEL(2) "\r", *c.base, "\033[", (int)c.pos + 1, "G");
}
putchar('\n');
memcpy(buf, c.base->s, c.base->len);
buf[c.base->len] = '\0';
free(c.base->s);
return ch == '\n';
}