gtk: add GtkTextRegion

GtkTextRegion is a helper for tracking regions of text when you might not
be able to use GtkTextBuffer's B-Tree. This can be useful when writing
code that needs to work with GtkEditable and GtkTextView both.

This is implemented in a fashion that resembles the combination of a
B+Tree (doubly-linked internal and leaf nodes) and a piecetable.

The goal for this is to be able to track regions of a buffer that need
updating as we process GtkTextView drawing.

A number of tests are provided to ensure correctness of the data structure.
This commit is contained in:
Christian Hergert
2021-03-29 13:38:13 -07:00
parent 3f6b2d9b9f
commit 1264245601
6 changed files with 2586 additions and 0 deletions

1293
gtk/gtktextregion.c Normal file

File diff suppressed because it is too large Load Diff

556
gtk/gtktextregionbtree.h Normal file
View File

@@ -0,0 +1,556 @@
/* gtktextregionbtree.h
*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* This file is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This file 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 Lesser 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_TEXT_REGION_BTREE_H__
#define __GTK_TEXT_REGION_BTREE_H__
#include "gtktextregionprivate.h"
G_BEGIN_DECLS
/* The following set of macros are used to create a queue similar to a
* double-ended linked list but using integers as indexes for items within the
* queue. Doing so allows for inserting or removing items from a b+tree node
* without having to memmove() data to maintain sorting orders.
*/
#define VAL_QUEUE_INVALID(Node) ((glib_typeof((Node)->head))-1)
#define VAL_QUEUE_LENGTH(Node) ((Node)->length)
#define VAL_QUEUE_EMPTY(Node) ((Node)->head == VAL_QUEUE_INVALID(Node))
#define VAL_QUEUE_PEEK_HEAD(Node) ((Node)->head)
#define VAL_QUEUE_PEEK_TAIL(Node) ((Node)->tail)
#define VAL_QUEUE_IS_VALID(Node, ID) ((ID) != VAL_QUEUE_INVALID(Node))
#define VAL_QUEUE_NODE(Type, N_Items) \
struct { \
Type length; \
Type head; \
Type tail; \
struct { \
Type prev; \
Type next; \
} items[N_Items]; \
}
#define VAL_QUEUE_INIT(Node) \
G_STMT_START { \
(Node)->length = 0; \
(Node)->head = VAL_QUEUE_INVALID(Node); \
(Node)->tail = VAL_QUEUE_INVALID(Node); \
for (guint _i = 0; _i < G_N_ELEMENTS ((Node)->items); _i++) \
{ \
(Node)->items[_i].next = VAL_QUEUE_INVALID(Node); \
(Node)->items[_i].prev = VAL_QUEUE_INVALID(Node); \
} \
} G_STMT_END
#ifndef G_DISABLE_ASSERT
# define _VAL_QUEUE_VALIDATE(Node) \
G_STMT_START { \
glib_typeof((Node)->head) count = 0; \
\
if ((Node)->tail != VAL_QUEUE_INVALID(Node)) \
g_assert_cmpint((Node)->items[(Node)->tail].next, ==, VAL_QUEUE_INVALID(Node)); \
if ((Node)->head != VAL_QUEUE_INVALID(Node)) \
g_assert_cmpint((Node)->items[(Node)->head].prev , ==, VAL_QUEUE_INVALID(Node)); \
\
for (glib_typeof((Node)->head) _viter = (Node)->head; \
VAL_QUEUE_IS_VALID(Node, _viter); \
_viter = (Node)->items[_viter].next) \
{ \
count++; \
} \
\
g_assert_cmpint(count, ==, (Node)->length); \
} G_STMT_END
#else
# define _VAL_QUEUE_VALIDATE(Node) G_STMT_START { } G_STMT_END
#endif
#define VAL_QUEUE_PUSH_HEAD(Node, ID) \
G_STMT_START { \
(Node)->items[ID].prev = VAL_QUEUE_INVALID(Node); \
(Node)->items[ID].next = (Node)->head; \
if (VAL_QUEUE_IS_VALID(Node, (Node)->head)) \
(Node)->items[(Node)->head].prev = ID; \
(Node)->head = ID; \
if (!VAL_QUEUE_IS_VALID(Node, (Node)->tail)) \
(Node)->tail = ID; \
(Node)->length++; \
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_PUSH_TAIL(Node, ID) \
G_STMT_START { \
(Node)->items[ID].prev = (Node)->tail; \
(Node)->items[ID].next = VAL_QUEUE_INVALID(Node); \
if (VAL_QUEUE_IS_VALID (Node, (Node)->tail)) \
(Node)->items[(Node)->tail].next = ID; \
(Node)->tail = ID; \
if (!VAL_QUEUE_IS_VALID(Node, (Node)->head)) \
(Node)->head = ID; \
(Node)->length++; \
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_INSERT(Node, Nth, Val) \
G_STMT_START { \
g_assert_cmpint (VAL_QUEUE_LENGTH(Node),<,G_N_ELEMENTS((Node)->items)); \
\
if ((Nth) == 0) \
{ \
VAL_QUEUE_PUSH_HEAD(Node, Val); \
} \
else if ((Nth) == (Node)->length) \
{ \
VAL_QUEUE_PUSH_TAIL(Node, Val); \
} \
else \
{ \
glib_typeof((Node)->head) ID; \
glib_typeof((Node)->head) _nth; \
\
g_assert_cmpint (VAL_QUEUE_LENGTH(Node), >, 0); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->head)); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->tail)); \
\
for (ID = (Node)->head, _nth = 0; \
_nth < (Nth) && VAL_QUEUE_IS_VALID(Node, ID); \
ID = (Node)->items[ID].next, ++_nth) \
{ /* Do Nothing */ } \
\
g_assert (VAL_QUEUE_IS_VALID(Node, ID)); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->items[ID].prev)); \
\
(Node)->items[Val].prev = (Node)->items[ID].prev; \
(Node)->items[Val].next = ID; \
(Node)->items[(Node)->items[ID].prev].next = Val; \
(Node)->items[ID].prev = Val; \
\
(Node)->length++; \
\
_VAL_QUEUE_VALIDATE(Node); \
} \
} G_STMT_END
#define VAL_QUEUE_POP_HEAD(Node,_pos) VAL_QUEUE_POP_NTH((Node), 0, _pos)
#define VAL_QUEUE_POP_TAIL(Node,_pos) VAL_QUEUE_POP_NTH((Node), (Node)->length - 1, _pos)
#define VAL_QUEUE_POP_AT(Node, _pos) \
G_STMT_START { \
g_assert (_pos != VAL_QUEUE_INVALID(Node)); \
g_assert (_pos < G_N_ELEMENTS ((Node)->items)); \
\
if ((Node)->items[_pos].prev != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[_pos].prev].next = (Node)->items[_pos].next; \
if ((Node)->items[_pos].next != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[_pos].next].prev = (Node)->items[_pos].prev; \
if ((Node)->head == _pos) \
(Node)->head = (Node)->items[_pos].next; \
if ((Node)->tail == _pos) \
(Node)->tail = (Node)->items[_pos].prev; \
\
(Node)->items[_pos].prev = VAL_QUEUE_INVALID((Node)); \
(Node)->items[_pos].next = VAL_QUEUE_INVALID((Node)); \
\
(Node)->length--; \
\
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_POP_NTH(Node, Nth, _pos) \
G_STMT_START { \
_pos = VAL_QUEUE_INVALID(Node); \
\
if (Nth == 0) \
_pos = (Node)->head; \
else if (Nth >= (((Node)->length) - 1)) \
_pos = (Node)->tail; \
else \
VAL_QUEUE_NTH (Node, Nth, _pos); \
\
if (_pos != VAL_QUEUE_INVALID(Node)) \
VAL_QUEUE_POP_AT (Node, _pos); \
} G_STMT_END
#define VAL_QUEUE_NTH(Node, Nth, _iter) \
G_STMT_START { \
glib_typeof((Node)->head) _nth; \
if (Nth == 0) \
_iter = (Node)->head; \
else if (Nth >= (((Node)->length) - 1)) \
_iter = (Node)->tail; \
else \
{ \
for (_iter = (Node)->head, _nth = 0; \
_nth < (Nth); \
_iter = (Node)->items[_iter].next, ++_nth) \
{ /* Do Nothing */ } \
} \
} G_STMT_END
#define _VAL_QUEUE_MOVE(Node, Old, New) \
G_STMT_START { \
(Node)->items[New] = (Node)->items[Old]; \
if ((Node)->items[New].prev != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[New].prev].next = New; \
if ((Node)->items[New].next != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[New].next].prev = New; \
if ((Node)->head == Old) \
(Node)->head = New; \
if ((Node)->tail == Old) \
(Node)->tail = New; \
} G_STMT_END
/*
* SORTED_ARRAY_FIELD:
* @TYPE: The type of the structure used by elements in the array
* @N_ITEMS: The maximum number of items in the array
*
* This creates a new inline structure that can be embedded within
* other super-structures.
*
* @N_ITEMS must be <= 254 or this macro will fail.
*/
#define SORTED_ARRAY_FIELD(TYPE,N_ITEMS) \
struct { \
TYPE items[N_ITEMS]; \
VAL_QUEUE_NODE(guint8, N_ITEMS) q; \
}
/*
* SORTED_ARRAY_INIT:
* @FIELD: A pointer to a SortedArray
*
* This will initialize a node that has been previously registered
* using %SORTED_ARRAY_FIELD(). You must call this macro before
* using the SortedArray structure.
*/
#define SORTED_ARRAY_INIT(FIELD) \
G_STMT_START { \
G_STATIC_ASSERT (G_N_ELEMENTS((FIELD)->items) < 255); \
VAL_QUEUE_INIT(&(FIELD)->q); \
} G_STMT_END
/*
* SORTED_ARRAY_LENGTH:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to the number of items inserted into
* the SortedArray.
*/
#define SORTED_ARRAY_LENGTH(FIELD) (VAL_QUEUE_LENGTH(&(FIELD)->q))
/*
* SORTED_ARRAY_CAPACITY:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to the number of elements in the SortedArray.
* This is dependent on how the SortedArray was instantiated using
* the %SORTED_ARRAY_FIELD() macro.
*/
#define SORTED_ARRAY_CAPACITY(FIELD) (G_N_ELEMENTS((FIELD)->items))
/*
* SORTED_ARRAY_IS_FULL:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to 1 if the SortedArray is at capacity.
* Otherwise, the macro will evaluate to 0.
*/
#define SORTED_ARRAY_IS_FULL(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == SORTED_ARRAY_CAPACITY(FIELD))
/*
* SORTED_ARRAY_IS_EMPTY:
* @FIELD: A SortedArray field
*
* This macro will evaluate to 1 if the SortedArray contains zero children.
*/
#define SORTED_ARRAY_IS_EMPTY(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == 0)
/*
* SORTED_ARRAY_INSERT_VAL:
* @FIELD: A pointer to a SortedArray field.
* @POSITION: the logical position at which to insert
* @ELEMENT: The element to insert
*
* This will insert a new item into the array. It is invalid API use
* to call this function while the SortedArray is at capacity. Check
* SORTED_ARRAY_IS_FULL() before using this function to be certain.
*/
#define SORTED_ARRAY_INSERT_VAL(FIELD,POSITION,ELEMENT) \
G_STMT_START { \
guint8 _pos; \
\
g_assert (POSITION >= 0); \
g_assert (POSITION <= SORTED_ARRAY_LENGTH(FIELD)); \
\
_pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert (_pos != VAL_QUEUE_INVALID(&(FIELD)->q)); \
(FIELD)->items[_pos] = ELEMENT; \
VAL_QUEUE_INSERT(&(FIELD)->q, POSITION, _pos); \
} G_STMT_END
#define SORTED_ARRAY_REMOVE_INDEX(FIELD,POSITION,_ele) \
G_STMT_START { \
guint8 _pos; \
guint8 _len; \
\
VAL_QUEUE_POP_NTH(&(FIELD)->q, POSITION, _pos); \
_ele = (FIELD)->items[_pos]; \
_len = VAL_QUEUE_LENGTH(&(FIELD)->q); \
\
/* We must preserve our invariant of having no empty gaps \
* in the array so that se can place new items always at the \
* end (to avoid scanning for an empty spot). \
* Therefore we move our tail item into the removed slot and \
* adjust the iqueue positions (which are all O(1). \
*/ \
\
if (_pos < _len) \
{ \
(FIELD)->items[_pos] = (FIELD)->items[_len]; \
_VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos); \
} \
} G_STMT_END
/* SORTED_ARRAY_FOREACH_REMOVE:
*
* This a form of SORTED_ARRAY_REMOVE_INDEX but to be used when you
* are within a SORTED_ARRAY_FOREACH() to avoid extra scanning.
*/
#define SORTED_ARRAY_FOREACH_REMOVE(FIELD) \
G_STMT_START { \
guint8 _pos = _current; \
guint8 _len = VAL_QUEUE_LENGTH(&(FIELD)->q); \
\
g_assert (_len > 0); \
g_assert (_pos < _len); \
VAL_QUEUE_POP_AT(&(FIELD)->q, _pos); \
g_assert (VAL_QUEUE_LENGTH(&(FIELD)->q) == _len-1); \
_len--; \
\
/* We must preserve our invariant of having no empty gaps \
* in the array so that se can place new items always at the \
* end (to avoid scanning for an empty spot). \
* Therefore we move our tail item into the removed slot and \
* adjust the iqueue positions (which are all O(1). \
*/ \
\
if (_pos < _len) \
{ \
(FIELD)->items[_pos] = (FIELD)->items[_len]; \
_VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos); \
\
/* We might need to change the iter if next position moved */ \
if (_aiter == _len) \
_aiter = _pos; \
} \
\
} G_STMT_END
/*
* SORTED_ARRAY_FOREACH:
* @FIELD: A pointer to a SortedArray
* @Element: The type of the elements in @FIELD
* @Name: the name for a pointer of type @Element
* @LABlock: a {} tyle block to execute for each item. You may use
* "break" to exit the foreach.
*
* Calls @Block for every element stored in @FIELD. A pointer to
* each element will be provided as a variable named @Name.
*/
#define SORTED_ARRAY_FOREACH(FIELD, Element, Name, LABlock) \
G_STMT_START { \
for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.head; \
_aiter != VAL_QUEUE_INVALID(&(FIELD)->q); \
/* Do Nothing */) \
{ \
G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter; \
Element * Name = &(FIELD)->items[_aiter]; \
_aiter = (FIELD)->q.items[_aiter].next; \
LABlock \
} \
} G_STMT_END
#define SORTED_ARRAY_FOREACH_REVERSE(FIELD, Element, Name, LABlock) \
G_STMT_START { \
for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.tail; \
_aiter != VAL_QUEUE_INVALID(&(FIELD)->q); \
/* Do Nothing */) \
{ \
G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter; \
Element * Name = &(FIELD)->items[_aiter]; \
_aiter = (FIELD)->q.items[_aiter].prev; \
LABlock \
} \
} G_STMT_END
#define SORTED_ARRAY_FOREACH_PEEK(FIELD) \
(((FIELD)->q.items[_current].next != VAL_QUEUE_INVALID(&(FIELD)->q)) \
? &(FIELD)->items[(FIELD)->q.items[_current].next] : NULL)
#define SORTED_ARRAY_SPLIT(FIELD, SPLIT) \
G_STMT_START { \
guint8 _mid; \
\
SORTED_ARRAY_INIT(SPLIT); \
\
_mid = SORTED_ARRAY_LENGTH(FIELD) / 2; \
\
for (guint8 _z = 0; _z < _mid; _z++) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(SPLIT, ele); \
} \
} G_STMT_END
#define SORTED_ARRAY_SPLIT2(FIELD, LEFT, RIGHT) \
G_STMT_START { \
guint8 mid; \
\
SORTED_ARRAY_INIT(LEFT); \
SORTED_ARRAY_INIT(RIGHT); \
\
mid = SORTED_ARRAY_LENGTH(FIELD) / 2; \
\
for (guint8 i = 0; i < mid; i++) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(RIGHT, ele); \
} \
\
while (!SORTED_ARRAY_IS_EMPTY(FIELD)) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(LEFT, ele); \
} \
} G_STMT_END
#define SORTED_ARRAY_PEEK_HEAD(FIELD) ((FIELD)->items[VAL_QUEUE_PEEK_HEAD(&(FIELD)->q)])
#define SORTED_ARRAY_POP_HEAD(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, 0, _ele)
#define SORTED_ARRAY_POP_TAIL(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, SORTED_ARRAY_LENGTH(FIELD)-1, _ele)
#define SORTED_ARRAY_PUSH_HEAD(FIELD, ele) \
G_STMT_START { \
guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items)); \
(FIELD)->items[_pos] = ele; \
VAL_QUEUE_PUSH_HEAD(&(FIELD)->q, _pos); \
} G_STMT_END
#define SORTED_ARRAY_PUSH_TAIL(FIELD, ele) \
G_STMT_START { \
guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items)); \
(FIELD)->items[_pos] = ele; \
VAL_QUEUE_PUSH_TAIL(&(FIELD)->q, _pos); \
} G_STMT_END
#define GTK_TEXT_REGION_MAX_BRANCHES 26
#define GTK_TEXT_REGION_MIN_BRANCHES (GTK_TEXT_REGION_MAX_BRANCHES/3)
#define GTK_TEXT_REGION_MAX_RUNS 26
#define GTK_TEXT_REGION_MIN_RUNS (GTK_TEXT_REGION_MAX_RUNS/3)
typedef union _GtkTextRegionNode GtkTextRegionNode;
typedef struct _GtkTextRegionBranch GtkTextRegionBranch;
typedef struct _GtkTextRegionLeaf GtkTextRegionLeaf;
typedef struct _GtkTextRegionChild GtkTextRegionChild;
struct _GtkTextRegionChild
{
GtkTextRegionNode *node;
gsize length;
};
struct _GtkTextRegionBranch
{
GtkTextRegionNode *tagged_parent;
GtkTextRegionNode *prev;
GtkTextRegionNode *next;
SORTED_ARRAY_FIELD (GtkTextRegionChild, GTK_TEXT_REGION_MAX_BRANCHES) children;
};
struct _GtkTextRegionLeaf
{
GtkTextRegionNode *tagged_parent;
GtkTextRegionNode *prev;
GtkTextRegionNode *next;
SORTED_ARRAY_FIELD (GtkTextRegionRun, GTK_TEXT_REGION_MAX_RUNS) runs;
};
union _GtkTextRegionNode
{
/* pointer to the parent, low bit 0x1 means leaf node */
GtkTextRegionNode *tagged_parent;
struct _GtkTextRegionLeaf leaf;
struct _GtkTextRegionBranch branch;
};
struct _GtkTextRegion
{
GtkTextRegionNode root;
GtkTextRegionJoinFunc join_func;
GtkTextRegionSplitFunc split_func;
gsize length;
GtkTextRegionNode *cached_result;
gsize cached_result_offset;
};
#define TAG(ptr,val) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr)|(gsize)val)
#define UNTAG(ptr) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr) & ~(gsize)1)
static inline GtkTextRegionNode *
gtk_text_region_node_get_parent (GtkTextRegionNode *node)
{
if (node == NULL)
return NULL;
return UNTAG (node->tagged_parent);
}
static inline gboolean
gtk_text_region_node_is_leaf (GtkTextRegionNode *node)
{
GtkTextRegionNode *parent = gtk_text_region_node_get_parent (node);
return parent != NULL && node->tagged_parent != parent;
}
static inline void
gtk_text_region_node_set_parent (GtkTextRegionNode *node,
GtkTextRegionNode *parent)
{
node->tagged_parent = TAG (parent, gtk_text_region_node_is_leaf (node));
}
static inline gsize
gtk_text_region_node_length (GtkTextRegionNode *node)
{
gsize length = 0;
g_assert (node != NULL);
if (gtk_text_region_node_is_leaf (node))
{
SORTED_ARRAY_FOREACH (&node->leaf.runs, GtkTextRegionRun, run, {
length += run->length;
});
}
else
{
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
length += child->length;
});
}
return length;
}
static inline GtkTextRegionNode *
_gtk_text_region_get_first_leaf (GtkTextRegion *self)
{
for (GtkTextRegionNode *iter = &self->root;
iter;
iter = SORTED_ARRAY_PEEK_HEAD (&iter->branch.children).node)
{
if (gtk_text_region_node_is_leaf (iter))
return iter;
}
g_assert_not_reached ();
}
G_END_DECLS
#endif /* __GTK_TEXT_REGION_BTREE_H__ */

111
gtk/gtktextregionprivate.h Normal file
View File

@@ -0,0 +1,111 @@
/* gtktextregionprivate.h
*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* This file is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This file 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 Lesser 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_TEXT_REGION_PRIVATE_H__
#define __GTK_TEXT_REGION_PRIVATE_H__
#include <glib.h>
G_BEGIN_DECLS
typedef struct _GtkTextRegion GtkTextRegion;
typedef struct _GtkTextRegionRun
{
gsize length;
gpointer data;
} GtkTextRegionRun;
typedef void (*GtkTextRegionForeachFunc) (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data);
/*
* GtkTextRegionJoinFunc:
*
* This callback is used to determine if two runs can be joined together.
* This is useful when you have similar data pointers between two runs
* and seeing them as one run is irrelevant to the code using the
* text region.
*
* The default calllback for joining will return %FALSE so that no joins
* may occur.
*
* Returns: %TRUE if the runs can be joined; otherwise %FALSE
*/
typedef gboolean (*GtkTextRegionJoinFunc) (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right);
/*
* GtkTextRegionSplitFunc:
*
* This function is responsible for splitting a run into two runs.
* This can happen a delete happens in the middle of a run.
*
* By default, @left will contain the run prior to the delete, and
* @right will contain the run after the delete.
*
* You can use the run lengths to determine where the delete was made
* using @offset which is an absolute offset from the beginning of the
* region.
*
* If you would like to keep a single run after the deletion, then
* set @right to contain a length of zero and add it's previous
* length to @left.
*
* All the length in @left and @right must be accounted for.
*
* This function is useful when using GtkTextRegion as a piecetable
* where you want to adjust the data pointer to point at a new
* section of an original or change buffer.
*/
typedef void (*GtkTextRegionSplitFunc) (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right);
GtkTextRegion *_gtk_text_region_new (GtkTextRegionJoinFunc join_func,
GtkTextRegionSplitFunc split_func);
void _gtk_text_region_insert (GtkTextRegion *region,
gsize offset,
gsize length,
gpointer data);
void _gtk_text_region_replace (GtkTextRegion *region,
gsize offset,
gsize length,
gpointer data);
void _gtk_text_region_remove (GtkTextRegion *region,
gsize offset,
gsize length);
guint _gtk_text_region_get_length (GtkTextRegion *region);
void _gtk_text_region_foreach (GtkTextRegion *region,
GtkTextRegionForeachFunc func,
gpointer user_data);
void _gtk_text_region_foreach_in_range (GtkTextRegion *region,
gsize begin,
gsize end,
GtkTextRegionForeachFunc func,
gpointer user_data);
void _gtk_text_region_free (GtkTextRegion *region);
G_END_DECLS
#endif /* __GTK_TEXT_REGION_PRIVATE_H__ */

View File

@@ -145,6 +145,7 @@ gtk_private_sources = files([
'gtkstyleproperty.c',
'gtktextbtree.c',
'gtktexthistory.c',
'gtktextregion.c',
'gtktextviewchild.c',
'timsort/gtktimsort.c',
'gtktrashmonitor.c',

View File

@@ -115,6 +115,7 @@ internal_tests = [
{ 'name': 'rbtree-crash' },
{ 'name': 'propertylookuplistmodel' },
{ 'name': 'rbtree' },
{ 'name': 'textregion' },
{ 'name': 'timsort' },
]

624
testsuite/gtk/textregion.c Normal file
View File

@@ -0,0 +1,624 @@
#include "gtktextregionprivate.h"
#include "gtktextregionbtree.h"
static void
assert_leaves_empty (GtkTextRegion *region)
{
GtkTextRegionNode *leaf = _gtk_text_region_get_first_leaf (region);
guint count = 0;
for (; leaf; leaf = leaf->leaf.next, count++)
{
GtkTextRegionNode *parent = gtk_text_region_node_get_parent (leaf);
guint length = gtk_text_region_node_length (leaf);
guint length_in_parent = 0;
SORTED_ARRAY_FOREACH (&parent->branch.children, GtkTextRegionChild, child, {
if (child->node == leaf)
{
length_in_parent = child->length;
break;
}
});
if (length || length_in_parent)
g_error ("leaf %p %u has length of %u in %u runs. Parent thinks it has length of %u.",
leaf, count, length, SORTED_ARRAY_LENGTH (&leaf->leaf.runs), length_in_parent);
}
}
static guint
count_leaves (GtkTextRegion *region)
{
GtkTextRegionNode *leaf = _gtk_text_region_get_first_leaf (region);
guint count = 0;
for (; leaf; leaf = leaf->leaf.next)
count++;
return count;
}
static guint
count_internal_recuse (GtkTextRegionNode *node)
{
guint count = 1;
g_assert (!gtk_text_region_node_is_leaf (node));
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
g_assert (child->node != NULL);
if (!gtk_text_region_node_is_leaf (child->node))
count += count_internal_recuse (child->node);
});
return count;
}
static guint
count_internal (GtkTextRegion *region)
{
return count_internal_recuse (&region->root);
}
G_GNUC_UNUSED static inline void
print_tree (GtkTextRegionNode *node,
guint depth)
{
for (guint i = 0; i < depth; i++)
g_print (" ");
g_print ("%p %s Length=%"G_GSIZE_MODIFIER"u Items=%u Prev<%p> Next<%p>\n",
node,
gtk_text_region_node_is_leaf (node) ? "Leaf" : "Branch",
gtk_text_region_node_length (node),
gtk_text_region_node_is_leaf (node) ?
SORTED_ARRAY_LENGTH (&node->leaf.runs) :
SORTED_ARRAY_LENGTH (&node->branch.children),
gtk_text_region_node_is_leaf (node) ? node->leaf.prev : node->branch.prev,
gtk_text_region_node_is_leaf (node) ? node->leaf.next : node->branch.next);
if (!gtk_text_region_node_is_leaf (node))
{
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
print_tree (child->node, depth+1);
});
}
}
static void
assert_empty (GtkTextRegion *region)
{
#if 0
print_tree (&region->root, 0);
#endif
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 0);
assert_leaves_empty (region);
g_assert_cmpint (1, ==, count_internal (region));
g_assert_cmpint (1, ==, count_leaves (region));
}
static void
non_overlapping_insert_remove_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
g_assert_cmpint (offset, ==, GPOINTER_TO_UINT (run->data));
}
static void
non_overlapping_insert_remove (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
assert_empty (region);
for (guint i = 0; i < 100000; i++)
{
_gtk_text_region_insert (region, i, 1, GUINT_TO_POINTER (i));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, i + 1);
}
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 100000);
_gtk_text_region_foreach (region, non_overlapping_insert_remove_cb, NULL);
for (guint i = 0; i < 100000; i++)
_gtk_text_region_remove (region, 100000-1-i, 1);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 0);
assert_empty (region);
_gtk_text_region_free (region);
}
typedef struct {
gsize offset;
gsize length;
gpointer data;
} SplitRunCheck;
typedef struct {
gsize index;
gsize count;
const SplitRunCheck *checks;
} SplitRun;
static void
split_run_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
SplitRun *state = user_data;
g_assert_cmpint (offset, ==, state->checks[state->index].offset);
g_assert_cmpint (run->length, ==, state->checks[state->index].length);
g_assert_true (run->data == state->checks[state->index].data);
state->index++;
}
static void
split_run (void)
{
static const SplitRunCheck checks[] = {
{ 0, 1, NULL },
{ 1, 1, GSIZE_TO_POINTER (1) },
{ 2, 1, NULL },
};
SplitRun state = { 0, 3, checks };
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
_gtk_text_region_insert (region, 0, 2, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 2);
_gtk_text_region_insert (region, 1, 1, GSIZE_TO_POINTER (1));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 3);
_gtk_text_region_foreach (region, split_run_cb, &state);
_gtk_text_region_free (region);
}
static gboolean
can_join_cb (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return left->data == right->data;
}
static void
no_split_run (void)
{
static const SplitRunCheck checks[] = {
{ 0, 3, NULL },
};
SplitRun state = { 0, 1, checks };
GtkTextRegion *region = _gtk_text_region_new (can_join_cb, NULL);
_gtk_text_region_insert (region, 0, 2, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 2);
_gtk_text_region_insert (region, 1, 1, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 3);
_gtk_text_region_foreach (region, split_run_cb, &state);
_gtk_text_region_free (region);
}
static void
random_insertion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
gsize expected = 0;
for (guint i = 0; i < 10000; i++)
{
guint pos = g_random_int_range (0, region->length + 1);
guint len = g_random_int_range (1, 20);
_gtk_text_region_insert (region, pos, len, GUINT_TO_POINTER (i));
expected += len;
}
g_assert_cmpint (expected, ==, region->length);
_gtk_text_region_replace (region, 0, region->length, NULL);
g_assert_cmpint (count_leaves (region), ==, 1);
g_assert_cmpint (count_internal (region), ==, 1);
_gtk_text_region_free (region);
}
static void
random_deletion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
_gtk_text_region_insert (region, 0, 10000, NULL);
while (region->length > 0)
{
guint pos = region->length > 1 ? g_random_int_range (0, region->length-1) : 0;
guint len = region->length - pos > 1 ? g_random_int_range (1, region->length - pos) : 1;
_gtk_text_region_remove (region, pos, len);
}
_gtk_text_region_free (region);
}
static void
random_insert_deletion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
guint expected = 0;
guint i = 0;
while (region->length < 10000)
{
guint pos = g_random_int_range (0, region->length + 1);
guint len = g_random_int_range (1, 20);
_gtk_text_region_insert (region, pos, len, GUINT_TO_POINTER (i));
expected += len;
i++;
}
g_assert_cmpint (expected, ==, region->length);
while (region->length > 0)
{
guint pos = region->length > 1 ? g_random_int_range (0, region->length-1) : 0;
guint len = region->length - pos > 1 ? g_random_int_range (1, region->length - pos) : 1;
g_assert (pos + len <= region->length);
_gtk_text_region_remove (region, pos, len);
}
_gtk_text_region_free (region);
}
static void
test_val_queue (void)
{
VAL_QUEUE_NODE(guint8, 32) field;
guint8 pos;
VAL_QUEUE_INIT (&field);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 32);
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_NTH (&field, i, pos);
g_assert_cmpint (pos, ==, i);
}
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_POP_HEAD (&field, pos);
g_assert_cmpint (pos, ==, i);
}
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 0);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 32);
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_POP_TAIL (&field, pos);
g_assert_cmpint (pos, ==, 31-i);
}
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 0);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
while (VAL_QUEUE_LENGTH (&field))
VAL_QUEUE_POP_NTH (&field, VAL_QUEUE_LENGTH (&field)/2, pos);
}
typedef struct {
int v;
} Dummy;
static void
sorted_array (void)
{
SORTED_ARRAY_FIELD (Dummy, 32) field;
Dummy d;
guint i;
SORTED_ARRAY_INIT (&field);
d.v = 0; SORTED_ARRAY_INSERT_VAL (&field, 0, d);
d.v = 2; SORTED_ARRAY_INSERT_VAL (&field, 1, d);
d.v = 1; SORTED_ARRAY_INSERT_VAL (&field, 1, d);
i = 0;
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 3);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i++);
});
g_assert_cmpint (i, ==, 3);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 0);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 1);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 2);
for (i = 0; i < 10; i++)
{ d.v = i * 2;
SORTED_ARRAY_INSERT_VAL (&field, i, d); }
for (i = 0; i < 10; i++)
{ d.v = i * 2 + 1;
SORTED_ARRAY_INSERT_VAL (&field, i*2+1, d); }
i = 0;
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 20);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i++);
});
g_assert_cmpint (i, ==, 20);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
(void)dummy;
SORTED_ARRAY_FOREACH_REMOVE (&field);
});
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 0);
for (i = 0; i < 32; i++)
{
d.v = i;
SORTED_ARRAY_PUSH_TAIL (&field, d);
}
g_assert_cmpint (32, ==, SORTED_ARRAY_LENGTH (&field));
i = 0;
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i);
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 32-i);
SORTED_ARRAY_FOREACH_REMOVE (&field);
i++;
});
g_assert_cmpint (0, ==, SORTED_ARRAY_LENGTH (&field));
for (i = 0; i < 32; i++)
{
d.v = i;
SORTED_ARRAY_PUSH_TAIL (&field, d);
}
g_assert_cmpint (32, ==, SORTED_ARRAY_LENGTH (&field));
i = 31;
SORTED_ARRAY_FOREACH_REVERSE (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i);
SORTED_ARRAY_REMOVE_INDEX (&field, i, d);
i--;
});
}
static gboolean
replace_part_of_long_run_join (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return FALSE;
}
static void
replace_part_of_long_run_split (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right)
{
left->data = run->data;
right->data = GSIZE_TO_POINTER (GPOINTER_TO_SIZE (run->data) + left->length);
}
static void
replace_part_of_long_run (void)
{
GtkTextRegion *region = _gtk_text_region_new (replace_part_of_long_run_join,
replace_part_of_long_run_split);
static const SplitRunCheck checks0[] = {
{ 0, 5, NULL },
};
static const SplitRunCheck checks1[] = {
{ 0, 1, NULL },
{ 1, 3, GSIZE_TO_POINTER (2) },
};
static const SplitRunCheck checks2[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 3, GSIZE_TO_POINTER (2) },
};
static const SplitRunCheck checks3[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 1, GSIZE_TO_POINTER (2) },
{ 3, 1, GSIZE_TO_POINTER (4) },
};
static const SplitRunCheck checks4[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 1, GSIZE_TO_POINTER (2) },
{ 3, 1, GSIZE_TO_POINTER ((1L<<31)|2) },
{ 4, 1, GSIZE_TO_POINTER (4) },
};
SplitRun state0 = { 0, 1, checks0 };
SplitRun state1 = { 0, 2, checks1 };
SplitRun state2 = { 0, 3, checks2 };
SplitRun state3 = { 0, 4, checks3 };
SplitRun state4 = { 0, 5, checks4 };
_gtk_text_region_insert (region, 0, 5, NULL);
_gtk_text_region_foreach (region, split_run_cb, &state0);
_gtk_text_region_remove (region, 1, 1);
_gtk_text_region_foreach (region, split_run_cb, &state1);
_gtk_text_region_insert (region, 1, 1, GSIZE_TO_POINTER ((1L<<31)|1));
_gtk_text_region_foreach (region, split_run_cb, &state2);
_gtk_text_region_remove (region, 3, 1);
_gtk_text_region_foreach (region, split_run_cb, &state3);
_gtk_text_region_insert (region, 3, 1, GSIZE_TO_POINTER ((1L<<31)|2));
_gtk_text_region_foreach (region, split_run_cb, &state4);
_gtk_text_region_free (region);
}
typedef struct
{
char *original;
char *changes;
GString *res;
} wordstate;
static void
word_foreach_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer data)
{
wordstate *state = data;
gsize sdata = GPOINTER_TO_SIZE (run->data);
gsize soff = sdata & ~(1L<<31);
char *src;
if (sdata == soff)
src = state->original;
else
src = state->changes;
#if 0
g_print ("%lu len %lu (%s at %lu) %s\n",
offset, run->length, sdata == soff ? "original" : "changes", soff,
sdata == soff && src[sdata] == '\n' ? "is-newline" : "");
#endif
g_string_append_len (state->res, src + soff, run->length);
}
static gboolean
join_word_cb (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return FALSE;
}
static void
split_word_cb (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right)
{
gsize sdata = GPOINTER_TO_SIZE (run->data);
left->data = run->data;
right->data = GSIZE_TO_POINTER (sdata + left->length);
}
static void
test_words_database (void)
{
GtkTextRegion *region = _gtk_text_region_new (join_word_cb, split_word_cb);
g_autofree char *contents = NULL;
g_autoptr(GString) str = g_string_new (NULL);
g_autoptr(GString) res = g_string_new (NULL);
const char *word;
const char *iter;
gsize len;
wordstate state;
if (!g_file_get_contents ("/usr/share/dict/words", &contents, &len, NULL))
{
g_test_skip ("Words database not available");
return;
}
/* 0 offset of base buffer */
_gtk_text_region_insert (region, 0, len, NULL);
/* For each each word, remove it and replace it with a word added to str.
* At the end we'll create the buffer and make sure we get the same.
*/
word = contents;
iter = contents;
for (;;)
{
if (*iter == 0)
break;
if (g_unichar_isspace (g_utf8_get_char (iter)))
{
gsize pos = str->len;
g_string_append_len (str, word, iter - word);
_gtk_text_region_replace (region, word - contents, iter - word, GSIZE_TO_POINTER ((1L<<31)|pos));
while (*iter && g_unichar_isspace (g_utf8_get_char (iter)))
iter = g_utf8_next_char (iter);
word = iter;
}
else
iter = g_utf8_next_char (iter);
}
state.original = contents;
state.changes = str->str;
state.res = res;
_gtk_text_region_foreach (region, word_foreach_cb, &state);
g_assert_true (g_str_equal (contents, res->str));
_gtk_text_region_free (region);
}
static void
foreach_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
guint *count = user_data;
g_assert_cmpint (GPOINTER_TO_SIZE (run->data), ==, offset);
(*count)++;
}
static void
foreach_in_range (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
guint count;
for (guint i = 0; i < 100000; i++)
{
_gtk_text_region_insert (region, i, 1, GUINT_TO_POINTER (i));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, i + 1);
}
count = 0;
_gtk_text_region_foreach_in_range (region, 0, 100000, foreach_cb, &count);
g_assert_cmpint (count, ==, 100000);
count = 0;
_gtk_text_region_foreach_in_range (region, 1000, 5000, foreach_cb, &count);
g_assert_cmpint (count, ==, 4000);
_gtk_text_region_replace (region, 0, 10000, NULL);
count = 0;
_gtk_text_region_foreach_in_range (region, 1000, 5000, foreach_cb, &count);
g_assert_cmpint (count, ==, 1);
_gtk_text_region_free (region);
}
int
main (int argc,
char *argv[])
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/Gtk/TextRegion/val_queue", test_val_queue);
g_test_add_func ("/Gtk/TextRegion/sorted_array", sorted_array);
g_test_add_func ("/Gtk/TextRegion/non_overlapping_insert_remove", non_overlapping_insert_remove);
g_test_add_func ("/Gtk/TextRegion/foreach_in_range", foreach_in_range);
g_test_add_func ("/Gtk/TextRegion/split_run", split_run);
g_test_add_func ("/Gtk/TextRegion/no_split_run", no_split_run);
g_test_add_func ("/Gtk/TextRegion/random_insertion", random_insertion);
g_test_add_func ("/Gtk/TextRegion/random_deletion", random_deletion);
g_test_add_func ("/Gtk/TextRegion/random_insert_deletion", random_insert_deletion);
g_test_add_func ("/Gtk/TextRegion/replace_part_of_long_run", replace_part_of_long_run);
g_test_add_func ("/Gtk/TextRegion/words_database", test_words_database);
return g_test_run ();
}