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数据结构中添加一段字符、删除一段字符
涉及到的数据结构操作有:
- 可扩容数组
- 单链表的搜索、增添、删除、合并操作
设计如下数据结构:
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