about summary refs log tree commit diff
path: root/edit.c
diff options
context:
space:
mode:
Diffstat (limited to 'edit.c')
-rw-r--r--edit.c404
1 files changed, 266 insertions, 138 deletions
diff --git a/edit.c b/edit.c
index c63e4a2..effb623 100644
--- a/edit.c
+++ b/edit.c
@@ -1,186 +1,314 @@
-/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+/* 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 Affero General Public License as published by
+ * 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 Affero General Public License for more details.
+ * GNU General Public License for more details.
  *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * 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 <err.h>
+#include <errno.h>
+#include <limits.h>
 #include <stdbool.h>
 #include <stdlib.h>
-#include <sysexits.h>
 #include <wchar.h>
 #include <wctype.h>
 
-#include "chat.h"
-
-enum { BufLen = 512 };
-static struct {
-	wchar_t buf[BufLen];
-	wchar_t *ptr;
-	wchar_t *end;
-	wchar_t *tab;
-} line = {
-	.ptr = line.buf,
-	.end = line.buf,
-};
-
-const wchar_t *editHead(void) {
-	return line.buf;
-}
-const wchar_t *editTail(void) {
-	return line.ptr;
-}
+#include "edit.h"
 
-static void left(void) {
-	if (line.ptr > line.buf) line.ptr--;
-}
-static void right(void) {
-	if (line.ptr < line.end) line.ptr++;
+static bool isword(wchar_t ch) {
+	return !iswspace(ch) && !iswpunct(ch);
 }
 
-static void backWord(void) {
-	left();
-	wchar_t *word = wcsnrchr(line.buf, line.ptr - line.buf, L' ');
-	line.ptr = (word ? &word[1] : line.buf);
-}
-static void foreWord(void) {
-	right();
-	wchar_t *word = wcsnchr(line.ptr, line.end - line.ptr, L' ');
-	line.ptr = (word ? word : line.end);
+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;
 }
 
-static void insert(wchar_t ch) {
-	if (line.end == &line.buf[BufLen - 1]) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr + 1, line.ptr, line.end - line.ptr);
+int editReserve(struct Edit *e, size_t index, size_t count) {
+	if (index > e->len) {
+		errno = EINVAL;
+		return -1;
 	}
-	*line.ptr++ = ch;
-	line.end++;
-}
-static void backspace(void) {
-	if (line.ptr == line.buf) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr - 1, line.ptr, line.end - line.ptr);
+	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;
 	}
-	line.ptr--;
-	line.end--;
+	wmemmove(&e->buf[index + count], &e->buf[index], e->len - index);
+	e->len += count;
+	return 0;
 }
-static void delete(void) {
-	if (line.ptr == line.end) return;
-	right();
-	backspace();
+
+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;
 }
 
-static void killBackWord(void) {
-	wchar_t *from = line.ptr;
-	backWord();
-	wmemmove(line.ptr, from, line.end - from);
-	line.end -= from - line.ptr;
+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 void killForeWord(void) {
-	wchar_t *from = line.ptr;
-	foreWord();
-	wmemmove(from, line.ptr, line.end - line.ptr);
-	line.end -= line.ptr - from;
-	line.ptr = from;
+
+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 char *prefix;
-static void complete(struct Tag tag) {
-	if (!line.tab) {
-		line.tab = wcsnrchr(line.buf, line.ptr - line.buf, L' ');
-		line.tab = (line.tab ? &line.tab[1] : line.buf);
-		prefix = awcsntombs(line.tab, line.ptr - line.tab);
-		if (!prefix) err(EX_DATAERR, "awcstombs");
-	}
+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;
+}
 
-	const char *next = tabNext(tag, prefix);
-	if (!next) return;
+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++;
+		}
 
-	wchar_t *wcs = ambstowcs(next);
-	if (!wcs) err(EX_DATAERR, "ambstowcs");
+		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);
+		}
 
-	size_t i = 0;
-	for (; wcs[i] && line.ptr > &line.tab[i]; ++i) {
-		line.tab[i] = wcs[i];
-	}
-	while (line.ptr > &line.tab[i]) {
-		backspace();
-	}
-	for (; wcs[i]; ++i) {
-		insert(wcs[i]);
-	}
-	free(wcs);
-
-	size_t pos = line.tab - line.buf;
-	if (!pos && line.tab[0] != L'/') {
-		insert(L':');
-	} else if (pos >= 2) {
-		if (line.buf[pos - 2] == L':') {
-			line.buf[pos - 2] = L',';
-			insert(L':');
+		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;
 	}
-	insert(L' ');
+	return ret;
 }
 
-static void accept(void) {
-	if (!line.tab) return;
-	line.tab = NULL;
-	free(prefix);
-	tabAccept();
+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;
 }
-static void reject(void) {
-	if (!line.tab) return;
-	line.tab = NULL;
-	free(prefix);
-	tabReject();
+
+#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 void enter(struct Tag tag) {
-	if (line.end == line.buf) return;
-	*line.end = L'\0';
-	char *str = awcstombs(line.buf);
-	if (!str) err(EX_DATAERR, "awcstombs");
-	input(tag, str);
-	free(str);
-	line.ptr = line.buf;
-	line.end = line.buf;
+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);
 }
 
-void edit(struct Tag tag, enum Edit op, wchar_t ch) {
-	switch (op) {
-		break; case EditLeft:  reject(); left();
-		break; case EditRight: reject(); right();
-		break; case EditHome:  reject(); line.ptr = line.buf;
-		break; case EditEnd:   reject(); line.ptr = line.end;
+#define editFn(...) assert(0 == editFn(__VA_ARGS__))
 
-		break; case EditBackWord: reject(); backWord();
-		break; case EditForeWord: reject(); foreWord();
+int main(void) {
+	struct Edit cut = {0};
+	struct Edit e = { .cut = &cut };
 
-		break; case EditInsert:    accept(); insert(ch);
-		break; case EditBackspace: reject(); backspace();
-		break; case EditDelete:    reject(); delete();
+	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"));
 
-		break; case EditKill:         reject(); line.ptr = line.end = line.buf;
-		break; case EditKillBackWord: reject(); killBackWord();
-		break; case EditKillForeWord: reject(); killForeWord();
-		break; case EditKillEnd:      reject(); line.end = line.ptr;
+	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"));
 
-		break; case EditComplete: complete(tag);
+	fix(&e, "foo bar");
+	editFn(&e, EditPrevWord);
+	editFn(&e, EditDeleteHead);
+	assert(eq(&e, "\0bar"));
 
-		break; case EditEnter: accept(); enter(tag);
-	}
+	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"));
 
-	*line.end = L'\0';
+	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 */