315 lines
8.2 KiB
C
315 lines
8.2 KiB
C
/* Copyright (C) 2020, 2022 June McEnroe <june@causal.agency>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* Additional permission under GNU GPL version 3 section 7:
|
|
*
|
|
* If you modify this Program, or any covered work, by linking or
|
|
* combining it with OpenSSL (or a modified version of that library),
|
|
* containing parts covered by the terms of the OpenSSL License and the
|
|
* original SSLeay license, the licensors of this Program grant you
|
|
* additional permission to convey the resulting work. Corresponding
|
|
* Source for a non-source form of such a combination shall include the
|
|
* source code for the parts of OpenSSL used as well as that of the
|
|
* covered work.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <wchar.h>
|
|
#include <wctype.h>
|
|
|
|
#include "edit.h"
|
|
|
|
static bool isword(wchar_t ch) {
|
|
return !iswspace(ch) && !iswpunct(ch);
|
|
}
|
|
|
|
char *editString(const struct Edit *e, char **buf, size_t *cap, size_t *pos) {
|
|
size_t req = e->len * MB_CUR_MAX + 1;
|
|
if (req > *cap) {
|
|
char *new = realloc(*buf, req);
|
|
if (!new) return NULL;
|
|
*buf = new;
|
|
*cap = req;
|
|
}
|
|
|
|
const wchar_t *ptr = e->buf;
|
|
size_t len = wcsnrtombs(*buf, &ptr, e->pos, *cap-1, NULL);
|
|
if (len == (size_t)-1) return NULL;
|
|
if (pos) *pos = len;
|
|
|
|
ptr = &e->buf[e->pos];
|
|
size_t n = wcsnrtombs(
|
|
*buf + len, &ptr, e->len - e->pos, *cap-1 - len, NULL
|
|
);
|
|
if (n == (size_t)-1) return NULL;
|
|
len += n;
|
|
|
|
(*buf)[len] = '\0';
|
|
return *buf;
|
|
}
|
|
|
|
int editReserve(struct Edit *e, size_t index, size_t count) {
|
|
if (index > e->len) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
if (e->len + count > e->cap) {
|
|
size_t cap = (e->cap ?: 256);
|
|
while (cap < e->len + count) cap *= 2;
|
|
wchar_t *buf = realloc(e->buf, sizeof(*buf) * cap);
|
|
if (!buf) return -1;
|
|
e->buf = buf;
|
|
e->cap = cap;
|
|
}
|
|
wmemmove(&e->buf[index + count], &e->buf[index], e->len - index);
|
|
e->len += count;
|
|
return 0;
|
|
}
|
|
|
|
int editCopy(struct Edit *e, size_t index, size_t count) {
|
|
if (index + count > e->len) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
if (!e->cut) return 0;
|
|
e->cut->len = 0;
|
|
if (editReserve(e->cut, 0, count) < 0) return -1;
|
|
wmemcpy(e->cut->buf, &e->buf[index], count);
|
|
return 0;
|
|
}
|
|
|
|
int editDelete(struct Edit *e, bool cut, size_t index, size_t count) {
|
|
if (index + count > e->len) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
if (cut && editCopy(e, index, count) < 0) return -1;
|
|
wmemmove(&e->buf[index], &e->buf[index + count], e->len - index - count);
|
|
e->len -= count;
|
|
if (e->pos > e->len) e->pos = e->len;
|
|
return 0;
|
|
}
|
|
|
|
static size_t prevSpacing(const struct Edit *e, size_t pos) {
|
|
if (!pos) return 0;
|
|
do {
|
|
pos--;
|
|
} while (pos && !wcwidth(e->buf[pos]));
|
|
return pos;
|
|
}
|
|
|
|
static size_t nextSpacing(const struct Edit *e, size_t pos) {
|
|
if (pos == e->len) return e->len;
|
|
do {
|
|
pos++;
|
|
} while (pos < e->len && !wcwidth(e->buf[pos]));
|
|
return pos;
|
|
}
|
|
|
|
int editFn(struct Edit *e, enum EditFn fn) {
|
|
int ret = 0;
|
|
switch (fn) {
|
|
break; case EditHead: e->pos = 0;
|
|
break; case EditTail: e->pos = e->len;
|
|
break; case EditPrev: e->pos = prevSpacing(e, e->pos);
|
|
break; case EditNext: e->pos = nextSpacing(e, e->pos);
|
|
break; case EditPrevWord: {
|
|
while (e->pos && !isword(e->buf[e->pos-1])) e->pos--;
|
|
while (e->pos && isword(e->buf[e->pos-1])) e->pos--;
|
|
}
|
|
break; case EditNextWord: {
|
|
while (e->pos < e->len && isword(e->buf[e->pos])) e->pos++;
|
|
while (e->pos < e->len && !isword(e->buf[e->pos])) e->pos++;
|
|
}
|
|
|
|
break; case EditDeleteHead: {
|
|
ret = editDelete(e, true, 0, e->pos);
|
|
e->pos = 0;
|
|
}
|
|
break; case EditDeleteTail: {
|
|
ret = editDelete(e, true, e->pos, e->len - e->pos);
|
|
}
|
|
break; case EditDeletePrev: {
|
|
size_t prev = prevSpacing(e, e->pos);
|
|
editDelete(e, false, prev, e->pos - prev);
|
|
e->pos = prev;
|
|
}
|
|
break; case EditDeleteNext: {
|
|
editDelete(e, false, e->pos, nextSpacing(e, e->pos) - e->pos);
|
|
}
|
|
break; case EditDeletePrevWord: {
|
|
if (!e->pos) break;
|
|
size_t word = e->pos;
|
|
while (word && !isword(e->buf[word-1])) word--;
|
|
while (word && isword(e->buf[word-1])) word--;
|
|
ret = editDelete(e, true, word, e->pos - word);
|
|
e->pos = word;
|
|
}
|
|
break; case EditDeleteNextWord: {
|
|
if (e->pos == e->len) break;
|
|
size_t word = e->pos;
|
|
while (word < e->len && !isword(e->buf[word])) word++;
|
|
while (word < e->len && isword(e->buf[word])) word++;
|
|
ret = editDelete(e, true, e->pos, word - e->pos);
|
|
}
|
|
|
|
break; case EditPaste: {
|
|
if (!e->cut) break;
|
|
ret = editReserve(e, e->pos, e->cut->len);
|
|
if (ret == 0) {
|
|
wmemcpy(&e->buf[e->pos], e->cut->buf, e->cut->len);
|
|
e->pos += e->cut->len;
|
|
}
|
|
}
|
|
break; case EditTranspose: {
|
|
if (e->len < 2) break;
|
|
if (!e->pos) e->pos++;
|
|
if (e->pos == e->len) e->pos--;
|
|
wchar_t x = e->buf[e->pos-1];
|
|
e->buf[e->pos-1] = e->buf[e->pos];
|
|
e->buf[e->pos++] = x;
|
|
}
|
|
break; case EditCollapse: {
|
|
size_t ws;
|
|
for (e->pos = 0; e->pos < e->len;) {
|
|
for (; e->pos < e->len && !iswspace(e->buf[e->pos]); ++e->pos);
|
|
for (ws = e->pos; ws < e->len && iswspace(e->buf[ws]); ++ws);
|
|
if (e->pos && ws < e->len) {
|
|
editDelete(e, false, e->pos, ws - e->pos - 1);
|
|
e->buf[e->pos++] = L' ';
|
|
} else {
|
|
editDelete(e, false, e->pos, ws - e->pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
break; case EditClear: e->len = e->pos = 0;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int editInsert(struct Edit *e, wchar_t ch) {
|
|
char mb[MB_LEN_MAX];
|
|
if (wctomb(mb, ch) < 0) return -1;
|
|
if (editReserve(e, e->pos, 1) < 0) return -1;
|
|
e->buf[e->pos++] = ch;
|
|
return 0;
|
|
}
|
|
|
|
#ifdef TEST
|
|
#undef NDEBUG
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
static void fix(struct Edit *e, const char *str) {
|
|
assert(0 == editFn(e, EditClear));
|
|
for (const char *ch = str; *ch; ++ch) {
|
|
assert(0 == editInsert(e, (wchar_t)*ch));
|
|
}
|
|
}
|
|
|
|
static bool eq(struct Edit *e, const char *str1) {
|
|
size_t pos;
|
|
static size_t cap;
|
|
static char *buf;
|
|
assert(NULL != editString(e, &buf, &cap, &pos));
|
|
const char *str2 = &str1[strlen(str1) + 1];
|
|
return pos == strlen(str1)
|
|
&& !strncmp(buf, str1, pos)
|
|
&& !strcmp(&buf[pos], str2);
|
|
}
|
|
|
|
#define editFn(...) assert(0 == editFn(__VA_ARGS__))
|
|
|
|
int main(void) {
|
|
struct Edit cut = {0};
|
|
struct Edit e = { .cut = &cut };
|
|
|
|
fix(&e, "foo bar");
|
|
editFn(&e, EditHead);
|
|
assert(eq(&e, "\0foo bar"));
|
|
editFn(&e, EditTail);
|
|
assert(eq(&e, "foo bar\0"));
|
|
editFn(&e, EditPrev);
|
|
assert(eq(&e, "foo ba\0r"));
|
|
editFn(&e, EditNext);
|
|
assert(eq(&e, "foo bar\0"));
|
|
|
|
fix(&e, "foo, bar");
|
|
editFn(&e, EditPrevWord);
|
|
assert(eq(&e, "foo, \0bar"));
|
|
editFn(&e, EditPrevWord);
|
|
assert(eq(&e, "\0foo, bar"));
|
|
editFn(&e, EditNextWord);
|
|
assert(eq(&e, "foo, \0bar"));
|
|
editFn(&e, EditNextWord);
|
|
assert(eq(&e, "foo, bar\0"));
|
|
|
|
fix(&e, "foo bar");
|
|
editFn(&e, EditPrevWord);
|
|
editFn(&e, EditDeleteHead);
|
|
assert(eq(&e, "\0bar"));
|
|
|
|
fix(&e, "foo bar");
|
|
editFn(&e, EditPrevWord);
|
|
editFn(&e, EditDeleteTail);
|
|
assert(eq(&e, "foo \0"));
|
|
|
|
fix(&e, "foo bar");
|
|
editFn(&e, EditDeletePrev);
|
|
assert(eq(&e, "foo ba\0"));
|
|
editFn(&e, EditHead);
|
|
editFn(&e, EditDeleteNext);
|
|
assert(eq(&e, "\0oo ba"));
|
|
|
|
fix(&e, "foo, bar");
|
|
editFn(&e, EditDeletePrevWord);
|
|
assert(eq(&e, "foo, \0"));
|
|
editFn(&e, EditDeletePrevWord);
|
|
assert(eq(&e, "\0"));
|
|
|
|
fix(&e, "foo, bar");
|
|
editFn(&e, EditHead);
|
|
editFn(&e, EditDeleteNextWord);
|
|
assert(eq(&e, "\0, bar"));
|
|
editFn(&e, EditDeleteNextWord);
|
|
assert(eq(&e, "\0"));
|
|
|
|
fix(&e, "foo bar");
|
|
editFn(&e, EditDeletePrevWord);
|
|
editFn(&e, EditPaste);
|
|
assert(eq(&e, "foo bar\0"));
|
|
editFn(&e, EditPaste);
|
|
assert(eq(&e, "foo barbar\0"));
|
|
|
|
fix(&e, "bar");
|
|
editFn(&e, EditTranspose);
|
|
assert(eq(&e, "bra\0"));
|
|
editFn(&e, EditHead);
|
|
editFn(&e, EditTranspose);
|
|
assert(eq(&e, "rb\0a"));
|
|
editFn(&e, EditTranspose);
|
|
assert(eq(&e, "rab\0"));
|
|
|
|
fix(&e, " foo bar ");
|
|
editFn(&e, EditCollapse);
|
|
assert(eq(&e, "foo bar\0"));
|
|
}
|
|
|
|
#endif /* TEST */
|