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:
1293
gtk/gtktextregion.c
Normal file
1293
gtk/gtktextregion.c
Normal file
File diff suppressed because it is too large
Load Diff
556
gtk/gtktextregionbtree.h
Normal file
556
gtk/gtktextregionbtree.h
Normal 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
111
gtk/gtktextregionprivate.h
Normal 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__ */
|
||||
@@ -145,6 +145,7 @@ gtk_private_sources = files([
|
||||
'gtkstyleproperty.c',
|
||||
'gtktextbtree.c',
|
||||
'gtktexthistory.c',
|
||||
'gtktextregion.c',
|
||||
'gtktextviewchild.c',
|
||||
'timsort/gtktimsort.c',
|
||||
'gtktrashmonitor.c',
|
||||
|
||||
@@ -115,6 +115,7 @@ internal_tests = [
|
||||
{ 'name': 'rbtree-crash' },
|
||||
{ 'name': 'propertylookuplistmodel' },
|
||||
{ 'name': 'rbtree' },
|
||||
{ 'name': 'textregion' },
|
||||
{ 'name': 'timsort' },
|
||||
]
|
||||
|
||||
|
||||
624
testsuite/gtk/textregion.c
Normal file
624
testsuite/gtk/textregion.c
Normal 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 (®ion->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 (®ion->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 ();
|
||||
}
|
||||
Reference in New Issue
Block a user