piece table 的C语言简单实现

piece table 的C语言简单实现

piece table的介绍

piece table是文本编辑器领域的一个很重要的数据结构

能实现文本编辑器的数据结构有很多,例如vi编辑器远古版本使用的一整块数组、块状链表、行链表、数组的改进GAP BUFFER等,还有本文要介绍的piece table。

关于文本编辑器的各种实现方式,这篇论文给出了很详细的介绍
https://pan.baidu.com/s/1tNuJ6trAStnr52z1QZDR4A

关于piece table,这篇文章给出了很详细的说明
https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation

这里简要说明什么是piece table
主要参考了这篇文章:
https://zhuanlan.zhihu.com/p/259387658

piece table 由三部分构成:
源文本、输入缓存、pieces

源文本记录源文件的内容,它是一个只读字符串

输入缓存记录每一次输入的内容。它是一个只增字符串(append only)

pieces是piece table的精华
它的作用是指示哪些是应当出现在渲染界面的内容

举个例子,源文件内容为:

hello
01234

最开始的时候pieces记录的内容如下:

type	|start	|len
SOURCE	|0		|5

此时渲染的内容为hello
我们往输入缓存中添加, world

, world
0123456

要使渲染层输出内容为hello, world,需要将pieces记录的内容改为:

type	|start	|len
SOURCE	|0		|5
ADDBUF	|0		|7

此时要删除llo,,需要将pieces记录的内容更改为

type	|start	|len
SOURCE	|0		|2
ADDBUF	|1		|7

此时的输出为he world

pieces就像是手电筒一样,照亮我们需要的字符,如要删除某些字符,只要简单地将它的范围缩小即可。

piece table比起其他数据结构,优越的地方在于它可以迅速实现撤回和重做功能,无需复制粘贴大量数据
使用一个栈来存储每次的操作,只需要对table链表执行对应的操作即可迅速实现撤回与重做功能。

另外,可以使用另一个结构来存储每一行开始的位置等等。不在本文的考虑范围内。

C语言的简单实现

本次主要实现的功能为:在piecetable数据结构中添加一段字符、删除一段字符

涉及到的数据结构操作有:

  1. 可扩容数组
  2. 单链表的搜索、增添、删除、合并操作

设计如下数据结构:

typedef struct buf_t
{
    
    
	char *data;
	size_t curSize;
	size_t maxSize;
}buf_t;  // 变长缓存
typedef enum
{
    
    
	SOURCE = 0,
	ADDBUF,
	UNKNOW
}buf_e;
typedef struct table_t
{
    
    
	buf_e type;
	size_t start;
	size_t len;
	struct table_t *next;
}table_t;  // 单链表结构
typedef struct piecetable_t
{
    
    
	buf_t addbuf;  // append only
	char *source;  // read only
	table_t *table;  // linked list
}piecetable_t;

将输入缓存设置为可扩容类型的

注意其中的table_t类型,我将其设置为无表头的单向链表
实际上有表头的链表更好写一点

在实际使用中,可以使用红黑数来重写table_t类型的所有操作,将时间复杂度从O(n)提升到O(ln n)

创建与销毁

设计如下函数

// 创建新的piecetable结构
piecetable_t *piecetable_new(char *srcTxt);
// 销毁该piecetable
bool piecetable_free(piecetable_t *pt);

/**
 * 创建新的piecetable类型
 * @param  srcTxt 源文件指针
 */
piecetable_t *piecetable_new(char *srcTxt)
{
    
    
	piecetable_t *pt = (piecetable_t *)
		malloc(sizeof(piecetable_t));
	if (pt == NULL)
		return NULL;
	pt->source = srcTxt;  // 只读
	pt->table = table_init(srcTxt);  // 无表头的单链表结构
	buf_init(&(pt->addbuf));  // 可扩容的缓存数组
	return pt;
}

/**
 * 销毁piecetable
 * @param  pt 要销毁的对象
 * @return    是否销毁成功
 */
bool piecetable_free(piecetable_t *pt)
{
    
    
	free(pt->source);
	buf_free(&(pt->addbuf));
	table_free(pt->table);
	free(pt);
	return true;
}

具体函数实现细节请看等一下贴出来的源码

添加一段字符串

设置如下函数来添加字符串

// 往pt的pos处后面插入大小为size的字符串
bool piecetable_ins(
	piecetable_t *pt, size_t pos, char *s, size_t size);
/**
 * 往pt的pos处后面插入大小为size的字符串
 * @param  pt
 * @param  pos  插入位置
 * @param  s    要插入的字符串
 * @param  size 插入的字符串的长度
 * @return      是否插入成功
 */
bool piecetable_ins(
	piecetable_t *pt, size_t pos, char *s, size_t size)
{
    
    
	if (!pt || !s || size == 0)
		return false;
	if (!buf_ins(&(pt->addbuf), s, size))
		return false;
	table_t *tmp = table_ins(
		pt->table, pos, ADDBUF,
		pt->addbuf.curSize - size, size);
	if (!tmp)
		return false;
	pt->table = table_merge(tmp);
	return true;
}

往piece table中添加一串字符,比较简单的实现方式是直接添加到addbuf中,然后再将新的位置添加到table结构的记录中

但是,这么做在进行大量编辑的时候很容易把addbuf撑爆

比较理想的改进方式是,首先在addbuf中寻找子串,直接把子串的位置传给table结构,找不到了再添加进去。
使用table_ins来将新的位置添加到table结构中。若添加失败返回NULL。添加成功则返回table结构的头指针。

下面table_merge的作用是,因为在table结构里面进行插入删除等操作,难免会出现一些极端情况,例如

type | start | len
XXX  |  0    |  0
XXX  |  0    |  0
XXX  |  0    |  0
XXX  |  0    |  0
XXX  |  0    |  0
...

或者

type | start | len
XXX  | 0     | 1
XXX  | 1     | 1
XXX  | 2     | 1
XXX  | 3     | 1
XXX  | 4     | 1
...

使用table_merge函数将这些部分整合起来

下面给出table_ins的具体实现

/**
 * 往table里面插入记录
 * @param  table
 * @param  pos   插入的位置
 * @param  type  新记录的类型
 * @param  start 开始的位置
 * @param  len   插入的长度
 * @return       插入是否成功
 */
static
table_t *table_ins(
	table_t *table, size_t pos,
	size_t type, size_t start, size_t len)
{
    
    
	size_t curLen = 0;
	table_t *prev = NULL;
	table_t *cur = table_findPos(table, pos, &curLen, &prev);
	if (cur == NULL)
		return NULL;
	// curLen should be bigger than pos
	pos -= curLen - cur->len;
	if (prev == NULL && cur->type == UNKNOW)  // first chain
	{
    
    
		cur->type = type;
		cur->start = start;
		cur->len = len;
		return cur;
	}
	table_t *left, *right;
	if (pos == 0)
	{
    
    
		if (prev == NULL)
			return table_new(type, start, len, cur);
		left = prev;
		right = cur;
	}
	else
	{
    
    
		left = cur;
		right = table_new(
			left->type, left->start + pos,
			left->len - pos, left->next);
		if (right == NULL)
			return NULL;
		left->len = pos;
	}
	table_t *tmp = table_new(type, start, len, right);
	if (tmp == NULL)
		return NULL;
	left->next = tmp;
	return table;
}

里面使用到了一个table_findPos函数

/**
 * 寻找处于位置pos处的table记录
 * @param  table
 * @param  pos   要寻找的位置
 * @return       要寻找的table节点
 */
static
table_t *table_findPos(
	table_t *table, size_t pos, size_t *curLen, table_t **prev)
{
    
    
	if (prev != NULL)
		*prev = NULL;
	if (table == NULL)
	{
    
    
		if (pos > 0)
			return NULL;
		return table_new(UNKNOW, 0, 0, NULL);
	}
	*curLen = table->len;
	while (*curLen <= pos)
	{
    
    
		if (prev != NULL)
			*prev = table;
		table = table->next;
		if (table == NULL)
		{
    
    
			if (*curLen == pos)
				return table_new(UNKNOW, 0, 0, NULL);
			else
				return NULL;
		}
		*curLen += table->len;
	}
	return table;
}

删除一段字符串

删除一段字符串的操作是添加一段字符串的反操作

/**
 * 从table链表结构映射的pos处删除长度为len的数据
 * @param  table [description]
 * @param  pos   [description]
 * @param  len   [description]
 * @return       [description]
 */
static table_t *table_del(
	table_t *table, size_t pos,size_t len)
{
    
    
	size_t curLen;
	table_t *start = table_findPos(table, pos, &curLen, NULL);
	table_t *end = start;
	size_t deleLen, tmpPos;
	if (start == NULL)
		return NULL;
	pos -= curLen - start->len;
	tmpPos = pos;
	while (len && end)
	{
    
    
		size_t surpLen = end->len - pos;
		deleLen = (surpLen >= len) ? len : surpLen;
		len -= deleLen;
		pos = 0;
		end = end->next;
	}
	if (end == NULL && len > 0)  // 没有找到结束节点,说明删除的长度过长
		return NULL;
	if (start == end)  // 在同一个节点上执行删除操作
	{
    
    
		// 需要拆分节点
		start->next = table_new(
			start->type, start->start + tmpPos + deleLen,
			start->len - tmpPos - deleLen, start->next);
		start->len = tmpPos;
	}
	else
	{
    
    
		table_t *tmp = start->next;
		start->next = end;
		start->len = tmpPos;
		if (end != NULL)
		{
    
    
			end->start += deleLen;
			end->len -= deleLen;
		}
		while (tmp != end)  // tmp必定不为空
		{
    
    
			table_t *next = tmp->next;
			free(tmp);
			tmp = next;
		}
	}
	return table;
}

实验

使用如下main函数设计一个简单的行文本编辑器
顾名思义,它只能编辑一行的内容,超出一行的内容就会有奇怪的表现

#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

char get_char()  // linux下非阻塞输入
{
    
    
	char c;
	struct termios newt, oldt;
	int tty = open("/dev/tty", O_RDONLY);
	tcgetattr(tty, &oldt);
	newt = oldt;
	newt.c_lflag &= ~(ICANON | ECHO);
	tcsetattr(tty, TCSANOW, &newt);
	read(tty, &c, 1);
	tcsetattr(tty, TCSANOW, &oldt);
	return c;
}

int main(int argc, char *argv[])
{
    
    
	char buf[BUFSIZ];
	char space[BUFSIZ];
	piecetable_t *pt = piecetable_new(NULL);

	char c[2];
	size_t pos = 0;
	char dot[10];
	char *filename = (argc > 1) ? argv[1] : "save.txt";
	memset(buf, '\0', BUFSIZ);
	for (int i = 0; i < BUFSIZ; i ++)
		space[i] = ' ';
	int maxLen = strlen(buf);
	while ((*c = get_char()) != '#')
	{
    
    
		c[1] = '\0';
		if (*c == 0x7f)
		{
    
    
			piecetable_del(pt, pos - 1, 1);
			pos = (pos == 0) ? pos : pos - 1;
			piecetable_map(pt, buf, 0, BUFSIZ);
			sprintf(dot, "\033[%ldD", pos + 1);
		}
		else
		{
    
    
			piecetable_ins(pt, pos ++, c, 1);
			piecetable_map(pt, buf, 0, BUFSIZ);
			sprintf(dot, "\033[%ldD", pos - 1);
		}
		size_t len = strlen(buf);
		maxLen = (maxLen > len) ? maxLen : len;
		write(1, dot, strlen(dot));
		write(1, space, maxLen);
		sprintf(dot, "\033[%dD", maxLen);
		write(1, dot, strlen(dot));
		write(1, buf, strlen(buf));
	}
	piecetable_free(pt);
	FILE *fp = fopen(filename, "w+");
	fprintf(fp, "%s", buf);
	fclose(fp);

	return 0;
}

这个程序只考虑了功能性,没有考虑最佳实现方案

编译时使用的makefile

all:
	make pieceTable

pieceTable: pieceTable.o
	gcc -Wall -g -o pieceTable pieceTable.o

pieceTable.o: pieceTable.c pieceTable.h
	gcc -Wall -g -D PIECETABLE_TEST -c pieceTable.c

clean:
	rm pieceTable.o

remake:
	make clean
	make all

源码

源码已经上传CSDN,关注我之后就能看到
https://download.csdn.net/download/weixin_45206746/14623055

猜你喜欢

转载自blog.csdn.net/weixin_45206746/article/details/112784620