diff --git a/include/QF/txtbuffer.h b/include/QF/txtbuffer.h new file mode 100644 index 000000000..e73c7257a --- /dev/null +++ b/include/QF/txtbuffer.h @@ -0,0 +1,98 @@ +/* + txtbuffer.h + + Text buffer for edit or scrollback + + Copyright (C) 2020 Bill Currie + + 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 2 + 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, write to: + + Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA + +*/ + +#ifndef __QF_txtbuffer_h +#define __QF_txtbuffer_h + +#include +#include + +/** \defgroup txtbuffer Text buffers + \ingroup utils +*/ +///@{ + +/// must be a power of 2 +#define TXTBUFFER_GROW 0x4000 + +/** Text buffer implementing efficient editing. +*/ +typedef struct txtbuffer_s { + struct txtbuffer_s *next; + char *text; + size_t textSize; ///< amount of text in the buffer + size_t bufferSize; ///< current capacity of the buffer + size_t gapOffset; ///< beginning of the gap + size_t gapSize; ///< size of gap. gapSize + textSize == bufferSize +} txtbuffer_t; + +/** Create a new, empty buffer. +*/ +txtbuffer_t *TextBuffer_Create (void); + +/** Destroy a buffer, freeing all resources connected to it. + \param buffer The buffer to destroy. +*/ +void TextBuffer_Destroy (txtbuffer_t *buffer); + +/** Insert a block of text at the specified offset. + + Text after the offset is moved to be after the inserted block of text. + The buffer is resized as necessary. + nul characters are not significant in that they do not mark the end of + the text to be inserted. + + \param buffer The buffer to be updated + \param offset The offset in the buffer at which to insert the text block + \param text The text block to be inserted. May contain nul ('\0') + characters. + \param text_len The number of characters to insert. + \return 1 for success, 0 for failure (offset not valid or out + of memory) +*/ +int TextBuffer_InsertAt (txtbuffer_t *buffer, size_t offset, + const char *text, size_t text_len); + +/** Delete a block of text from the buffer. + + The buffer is not resized. Rather, its capacity before resizing is require + is increased. + + \param buffer The buffer to be updated + \param offset The offset of the beginning of the text block to be deleted + \param len The amount of characters to be deleted. Values larger than + the amount of text in the buffer beyond the beginning of + block are truncated to the amount of remaining text. Thus + using excessivly large values sets the offset to be the + end of the buffer. + \return 1 for success, 0 for failure (offset not valid) +*/ +int TextBuffer_DeleteAt (txtbuffer_t *buffer, size_t offset, size_t len); + +///@} + +#endif//__QF_txtbuffer_h diff --git a/libs/util/Makefile.am b/libs/util/Makefile.am index 0c6421450..c35e4e305 100644 --- a/libs/util/Makefile.am +++ b/libs/util/Makefile.am @@ -53,7 +53,8 @@ libQFutil_la_SOURCES= \ fendian.c hash.c idparse.c info.c link.c llist.c \ mathlib.c mdfour.c mersenne.c msg.c pakfile.c plugin.c qargs.c qendian.c \ qfplist.c quakefs.c quakeio.c riff.c script.c segtext.c set.c sizebuf.c \ - string.c sys.c va.c ver_check.c vrect.c wad.c wadfile.c zone.c \ + string.c sys.c txtbuffer.c va.c ver_check.c vrect.c wad.c wadfile.c \ + zone.c \ $(dirent) $(fnmatch) $(getopt) EXTRA_DIST= $(fnmatch_src) $(getopt_src) diff --git a/libs/util/test/Makefile.am b/libs/util/test/Makefile.am index a7bdb5d4e..283ac561e 100644 --- a/libs/util/test/Makefile.am +++ b/libs/util/test/Makefile.am @@ -4,7 +4,7 @@ AM_CPPFLAGS= -I$(top_srcdir)/include check_PROGRAMS= \ test-bary test-cs test-dq test-half test-mat3 test-mat4 test-plist \ - test-qfs test-quat test-seb test-seg test-set test-vrect + test-qfs test-quat test-seb test-seg test-set test-txtbuffer test-vrect test_bary_SOURCES=test-bary.c test_bary_LDADD=$(top_builddir)/libs/util/libQFutil.la @@ -54,6 +54,10 @@ test_set_SOURCES=test-set.c test_set_LDADD=$(top_builddir)/libs/util/libQFutil.la test_set_DEPENDENCIES=$(top_builddir)/libs/util/libQFutil.la +test_txtbuffer_SOURCES=test-txtbuffer.c +test_txtbuffer_LDADD=$(top_builddir)/libs/util/libQFutil.la +test_txtbuffer_DEPENDENCIES=$(top_builddir)/libs/util/libQFutil.la + test_vrect_SOURCES=test-vrect.c test_vrect_LDADD=$(top_builddir)/libs/util/libQFutil.la test_vrect_DEPENDENCIES=$(top_builddir)/libs/util/libQFutil.la diff --git a/libs/util/test/test-txtbuffer.c b/libs/util/test/test-txtbuffer.c new file mode 100644 index 000000000..44ccebdc9 --- /dev/null +++ b/libs/util/test/test-txtbuffer.c @@ -0,0 +1,240 @@ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif +#include +#include + +#include "QF/txtbuffer.h" + +static size_t +check_text_ptr (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + return buffer->text != 0; +} + +static size_t +get_textSize (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + return buffer->textSize; +} + +static size_t +get_bufferSize (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + return buffer->bufferSize; +} + +static size_t +get_gapOffset (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + return buffer->gapOffset; +} + +static size_t +get_gapSize (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + return buffer->gapSize; +} + +static size_t +insert_text (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + if (TextBuffer_InsertAt (buffer, offset, txt, length)) { + memset (buffer->text + buffer->gapOffset, 0xff, buffer->gapSize); + return 1; + } + return 0; +} + +static size_t +delete_text (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + if (TextBuffer_DeleteAt (buffer, offset, length)) { + memset (buffer->text + buffer->gapOffset, 0xff, buffer->gapSize); + return 1; + } + return 0; +} + +static size_t +destroy_buffer (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + TextBuffer_Destroy (buffer); + return 0; +} + +typedef size_t (*test_func) (txtbuffer_t *buffer, size_t offset, + const char *text, size_t length); + +static const char test_text[] = { + "Guarding the entrance to the Grendal\n" + "Gorge is the Shadow Gate, a small keep\n" + "and monastary which was once the home\n" + "of the Shadow cult.\n\n" + "For years the Shadow Gate existed in\n" + "obscurity but after the cult discovered\n" + "the \3023\354\341\343\353\240\307\341\364\345 in the caves below\n" + "the empire took notice.\n" + "A batallion of Imperial Knights were\n" + "sent to the gate to destroy the cult\n" + "and claim the artifact for the King.", +}; +static const char empty[TXTBUFFER_GROW] = { }; + +static size_t +compare_text (txtbuffer_t *buffer, size_t offset, const char *txt, + size_t length) +{ + //printf("%zd %ld %zd\n", offset, txt - test_text, length); + size_t res = memcmp (buffer->text + offset, txt, length) == 0; + if (!res) { + for (size_t i = 0; i < length; i++) { + //printf ("%02x %02x\n", txt[i] & 0xff, buffer->text[offset + i] & 0xff); + } + } + return res; +} + +#define txtsize (sizeof (test_text) - 1) +#define txtsize0 txtsize +#define bufsize0 (TXTBUFFER_GROW) +#define gapoffs0 txtsize0 +#define gapsize0 (bufsize0 - txtsize0) +#define subtxtlen 200 +#define suboffs 200 +#define txtsize1 (txtsize0 + subtxtlen) +#define bufsize1 (TXTBUFFER_GROW) +#define gapoffs1 (suboffs + subtxtlen) +#define gapsize1 (bufsize1 - txtsize1) +#define deltxtlen 350 +#define deloffs 150 +#define txtsize2 (txtsize + subtxtlen - deltxtlen) +#define bufsize2 (TXTBUFFER_GROW) +#define gapoffs2 (deloffs) +#define gapsize2 (bufsize2 - txtsize2) +#define deltxtrem (txtsize2 - gapoffs2) +#define emptyoffs 370 +#define txtsize3 (txtsize + sizeof (empty)) +#define bufsize3 (2 * TXTBUFFER_GROW) +#define gapoffs3 (emptyoffs + sizeof (empty)) +#define gapsize3 (bufsize3 - txtsize3) +#define txtsize4 (txtsize + 2 * sizeof (empty)) +#define bufsize4 (3 * TXTBUFFER_GROW) +#define gapoffs4 (emptyoffs + sizeof (empty)) +#define gapsize4 (bufsize4 - txtsize4) + +struct { + test_func test; + size_t offset_param; + const char *text_param; + size_t length_param; + int test_expect; +} tests[] = { + { check_text_ptr, 0, 0, 0, 0 }, + { get_textSize, 0, 0, 0, 0 }, + { get_bufferSize, 0, 0, 0, 0 }, + { get_gapOffset, 0, 0, 0, 0 }, + { get_gapSize, 0, 0, 0, 0 }, + { insert_text, 0, test_text, txtsize, 1 }, + { get_textSize, 0, 0, 0, txtsize0 }, + { get_bufferSize, 0, 0, 0, bufsize0 }, + { get_gapOffset, 0, 0, 0, gapoffs0 }, + { get_gapSize, 0, 0, 0, gapsize0 }, + { compare_text, 0, test_text, txtsize, 1 }, + { insert_text, suboffs, test_text, subtxtlen, 1 }, + { get_textSize, 0, 0, 0, txtsize1 }, + { get_bufferSize, 0, 0, 0, bufsize1 }, + { get_gapOffset, 0, 0, 0, gapoffs1 }, + { get_gapSize, 0, 0, 0, gapsize1 }, + { compare_text, 0, test_text, txtsize, 0 }, + { compare_text, 0, test_text, suboffs, 1 }, + { compare_text, suboffs, test_text, subtxtlen, 1 }, + { compare_text, gapoffs1 + gapsize1, test_text + subtxtlen, txtsize - subtxtlen, 1 }, + { delete_text, deloffs, 0, deltxtlen, 1 }, + { get_textSize, 0, 0, 0, txtsize2 }, + { get_bufferSize, 0, 0, 0, bufsize2 }, + { get_gapOffset, 0, 0, 0, gapoffs2 }, + { get_gapSize, 0, 0, 0, gapsize2 }, + { compare_text, 0, test_text, suboffs, 0 }, + { compare_text, suboffs, test_text, subtxtlen, 0 }, + { compare_text, gapoffs1 + gapsize1, test_text + subtxtlen, txtsize - subtxtlen, 0 }, + { compare_text, 0, test_text, deloffs, 1 }, + { compare_text, gapoffs2 + gapsize2, test_text + txtsize - deltxtrem, deltxtrem, 1 }, + { compare_text, gapoffs2 + gapsize2 - 1, test_text + txtsize - deltxtrem - 1, 1, 0 }, + { delete_text, 0, 0, -1, 1 }, + { check_text_ptr, 0, 0, 0, 1 }, + { get_textSize, 0, 0, 0, 0 }, + { get_bufferSize, 0, 0, 0, bufsize2 }, + { get_gapOffset, 0, 0, 0, 0 }, + { get_gapSize, 0, 0, 0, bufsize2 }, + { insert_text, 1, test_text, txtsize, 0 }, + { get_textSize, 0, 0, 0, 0 }, + { get_bufferSize, 0, 0, 0, bufsize2 }, + { get_gapOffset, 0, 0, 0, 0 }, + { get_gapSize, 0, 0, 0, bufsize2 }, + { insert_text, 0, test_text, txtsize, 1 }, + { insert_text, 300, 0, 0, 1 }, + { get_textSize, 0, 0, 0, txtsize0 }, + { get_bufferSize, 0, 0, 0, bufsize2 }, + { get_gapOffset, 0, 0, 0, 300 }, + { get_gapSize, 0, 0, 0, gapsize0 }, + { compare_text, 0, test_text, 300, 1 }, + { compare_text, 300, test_text + 300, 1, 0 }, + { compare_text, 300 + gapsize0, test_text + 300, txtsize - 300, 1 }, + { compare_text, 300 + gapsize0 - 1, test_text + 300 - 1, 1, 0 }, + { insert_text, 350, 0, 0, 1 }, + { compare_text, (emptyoffs - 20) + gapsize0, test_text + (emptyoffs - 20), txtsize - (emptyoffs - 20), 1 }, + { get_textSize, 0, 0, 0, txtsize0 }, + { get_bufferSize, 0, 0, 0, bufsize2 }, + { get_gapOffset, 0, 0, 0, (emptyoffs - 20) }, + { get_gapSize, 0, 0, 0, gapsize0 }, + { insert_text, emptyoffs, empty, sizeof (empty), 1 }, + { get_textSize, 0, 0, 0, txtsize3 }, + { get_bufferSize, 0, 0, 0, bufsize3 }, + { get_gapOffset, 0, 0, 0, gapoffs3 }, + { get_gapSize, 0, 0, 0, gapsize3 }, + { insert_text, emptyoffs, empty, sizeof (empty), 1 }, + { get_textSize, 0, 0, 0, txtsize4 }, + { get_bufferSize, 0, 0, 0, bufsize4 }, + { get_gapOffset, 0, 0, 0, gapoffs4 }, + { get_gapSize, 0, 0, 0, gapsize4 }, + { destroy_buffer, 0, 0, 0, 0 }, +}; +#define num_tests (sizeof (tests) / sizeof (tests[0])) +int test_start_line = __LINE__ - num_tests - 2; + +int +main (int argc, const char **argv) +{ + size_t i; + int res = 0; + + txtbuffer_t *buffer = TextBuffer_Create (); + + for (i = 0; i < num_tests; i++) { + if (tests[i].test) { + int test_res; + test_res = tests[i].test (buffer, + tests[i].offset_param, + tests[i].text_param, + tests[i].length_param); + if (test_res != tests[i].test_expect) { + res |= 1; + printf ("test %d (line %d) failed\n", (int) i, + (int) i + test_start_line); + printf ("expect: %d\n", tests[i].test_expect); + printf ("got : %d\n", test_res); + continue; + } + } + } + return res; +} diff --git a/libs/util/txtbuffer.c b/libs/util/txtbuffer.c new file mode 100644 index 000000000..03ad06500 --- /dev/null +++ b/libs/util/txtbuffer.c @@ -0,0 +1,172 @@ +/* + txtbuffer.c + + Text buffer + + Copyright (C) 2020 Bill Currie + + 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 2 + 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, write to: + + Free Software Foundation, Inc. + 59 Temple Place - Suite 330 + Boston, MA 02111-1307, USA + +*/ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef HAVE_STRING_H +# include +#endif +#ifdef HAVE_STRINGS_H +# include +#endif + +#include "QF/alloc.h" +#include "QF/qtypes.h" +#include "QF/sys.h" +#include "QF/txtbuffer.h" + +#include "compat.h" + +static txtbuffer_t *txtbuffers_freelist; + +static char * +txtbuffer_open_gap (txtbuffer_t *buffer, size_t offset, size_t length) +{ + size_t len; + char *dst; + char *src; + + if (offset == buffer->gapOffset && length <= buffer->gapSize) { + return buffer->text + buffer->gapOffset; + } + if (length <= buffer->gapSize) { + // no resize needed + if (offset < buffer->gapOffset) { + len = buffer->gapOffset - offset; + dst = buffer->text + buffer->gapOffset + buffer->gapSize - len; + src = buffer->text + offset; + } else { + len = offset - buffer->gapOffset; + dst = buffer->text + buffer->gapOffset; + src = buffer->text + buffer->gapOffset + buffer->gapSize; + } + memmove (dst, src, len); + } else { + // need to resize the buffer + // grow the buffer by the difference in lengths rounded up to the + // next multiple of TXTBUFFER_GROW + size_t new_size = buffer->bufferSize + length - buffer->gapSize; + new_size = (new_size + TXTBUFFER_GROW - 1) & ~(TXTBUFFER_GROW - 1); + char *new_text = realloc (buffer->text, new_size); + + if (!new_text) { + return 0; + } + buffer->text = new_text; + + if (offset < buffer->gapOffset) { + // move the old post-gap to the end of the new buffer + len = buffer->bufferSize - (buffer->gapOffset + buffer->gapSize); + dst = buffer->text + new_size - len; + src = buffer->text + buffer->gapOffset + buffer->gapSize; + memmove (dst, src, len); + // move the remaining chunk to after the end of the new gap or + // just before the location of the previous move + len = buffer->gapOffset - offset; + dst -= len; + src = buffer->text + offset; + memmove (dst, src, len); + } else if (offset > buffer->gapOffset) { + // move the old post-offset to the end of the new buffer + len = buffer->bufferSize - (offset + buffer->gapSize); + dst = buffer->text + new_size - len; + src = buffer->text + offset + buffer->gapSize; + memmove (dst, src, len); + // move the old post-gap to offset block to before the new gap + len = offset - buffer->gapOffset; + dst = buffer->text + buffer->gapOffset; + src = buffer->text + buffer->gapOffset + buffer->gapSize; + memmove (dst, src, len); + } else { + // the gap only grew, did not move + len = buffer->bufferSize - (offset + buffer->gapSize); + dst = buffer->text + new_size - len; + src = buffer->text + buffer->gapOffset + buffer->gapSize; + memmove (dst, src, len); + } + buffer->gapSize += new_size - buffer->bufferSize; + buffer->bufferSize = new_size; + } + buffer->gapOffset = offset; + return buffer->text + buffer->gapOffset; +} + +VISIBLE txtbuffer_t * +TextBuffer_Create (void) +{ + txtbuffer_t *buffer; + ALLOC (16, txtbuffer_t, txtbuffers, buffer); + return buffer; +} + +VISIBLE void +TextBuffer_Destroy (txtbuffer_t *buffer) +{ + if (buffer->text) { + free (buffer->text); + } + FREE (txtbuffers, buffer); +} + +VISIBLE int +TextBuffer_InsertAt (txtbuffer_t *buffer, size_t offset, + const char *text, size_t text_len) +{ + char *dst; + + if (offset > buffer->textSize) { + return 0; + } + dst = txtbuffer_open_gap (buffer, offset, text_len); + if (!dst) { + return 0; + } + memcpy (dst, text, text_len); + buffer->textSize += text_len; + buffer->gapOffset += text_len; + buffer->gapSize -= text_len; + return 1; +} + +VISIBLE int +TextBuffer_DeleteAt (txtbuffer_t *buffer, size_t offset, size_t len) +{ + if (offset > buffer->textSize) { + return 0; + } + // only moves, does not resize, the gap if necessary + txtbuffer_open_gap (buffer, offset, buffer->gapSize); + // clamp len to the amount of text beyond offset + if (len > buffer->textSize - offset) { + len = buffer->textSize - offset; + } + // delete the text + buffer->gapSize += len; + buffer->textSize -= len; + return 1; +}