Add a buffer-gap text buffer

This should be good for text editing and working with text files in
general.
This commit is contained in:
Bill Currie 2020-02-24 17:30:33 +09:00
parent a30433fa9e
commit f7493fe8fb
5 changed files with 517 additions and 2 deletions

98
include/QF/txtbuffer.h Normal file
View file

@ -0,0 +1,98 @@
/*
txtbuffer.h
Text buffer for edit or scrollback
Copyright (C) 2020 Bill Currie <bill@taniwha.org>
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 <stdint.h>
#include <stdlib.h>
/** \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

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,240 @@
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <stdio.h>
#include <string.h>
#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;
}

172
libs/util/txtbuffer.c Normal file
View file

@ -0,0 +1,172 @@
/*
txtbuffer.c
Text buffer
Copyright (C) 2020 Bill Currie <bill@taniwha.org>
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 <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#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;
}