编辑器之间隙缓冲区
编辑器之间隙缓冲区
近一年多都在开发一个带有词法和语法分析的编辑器,其中在文本编辑缓冲区管理中,看了挺多有益的资料和文档,故拿出来总结并与大家分享一下。
以下对一些缓冲区做一下对比以说明为什么要用间隙缓冲区作为编辑器的解决方案。
1.字符数组缓冲区
如果使用普通的字符数组(char[])来作为缓冲区的话,很明显的会面临一个问题:当在数组中插入或删除一个字符时,会导致数组的后半部分的移动,一个极端的例子是在数组的开头插入一个字符将导致后面的Length-1的字符逐一向后移动一个位置。很明显地,这将导致严重的性能问题。
2.字符串缓冲区
也许我们可以没有必要照顾字符数组是如何移动的,因为String类 已经为我们做了很多工作,但了解String后会发现,其实String具有不变性,表面上的变化实质是通过创建新字符串来实现的,频繁的创建新对象将使用不少的资源。
3.链表缓冲区
链表的插入和删除操作代价是很小的,所以也许可以将缓冲区中的字符作为链表的节点来构造一个链表缓冲区,但如果编辑的文本较多时,即链表较长时,迭代到链表的指定位置处所花的时间将是难于忍受的,也就是说它不能像数组那样快速地索引。
4.间隙缓冲区
这是一种较好的方式。
间隙缓冲区其实也是一个数组,但我们会想办法来尽量避免频繁的字符移动。我们在插入符号(要插入或删除字符的位置)的左侧增加一个“间隙”。
假设编辑器中的文本为“Gap text buffer”,并且插入符号(当前编辑位置)在字符串“text”后面,那么它在缓冲区中的布局是这样的:“Gap text |||| buffer”,其中的“||||”就是我们插入的“间隙”。当我们要在当前编辑位置片新插入一个字符‘x’(“text ”后面),那么编辑器中的文本变成“Gap text x buffer”,而缓冲区中的布局变成:
“Gap text x||| buffer”其实我们可以发现此次插入操作并没有字符的移动(这是普通字符数组所不能达到的),没有新字符串的创建(这是String所不能达到的),而它仅仅是将该字符数组中的“间隙”的第一个‘|’替换成‘x’,即仅仅是“间隙”变短了。
同样的道理,我们发现想删除‘x’这个字符,只需要执行上述过程的逆过程,将该字符数组中的“间隙”变长,将字符‘x’替换成‘|’就可以了。可以发现在间隙缓冲区中,大多数的插入和删除操作仅仅导致了“间隙”的伸长或缩短,而缓冲区中的其他位置却是不发生任何变化的。
只有当插入的字符过多,导致“间隙”长度接近0时,我们才有必要重新分配空间以便将“间隙”回复到一个合理长度。比如我们可以将“间隙”默认长度设置为256,当插入了255个字符后,发现“间隙”很快将被用尽了,这时我们才重新分配空间来将“间隙”恢复至256,而这中分配次数相对与插入次数是微不足道的。同样只有当删除的字符过多,导致“间隙”过大的时候,我们才有必要“缩紧”数组以求占用更少的空间。
以下是源代码(C#的,有兴趣的可以转其它语言):
using System;
using System.Text;
namespace TextBufferStrategy
{
public class GapTextBuffer
{
char[] buffer = new char[0];
int gapBeginOffset = 0;
int gapEndOffset = 0;
int minGapLength = 32;
int maxGapLength = 256;
public int Length
{
get
{
return buffer.Length - GapLength;
}
}
int GapLength
{
get
{
return gapEndOffset - gapBeginOffset;
}
}
public void SetContent(string text)
{
if (text == null)
{
text = String.Empty;
}
buffer = text.ToCharArray();
gapBeginOffset = gapEndOffset = 0;
}
public char GetCharAt(int offset)
{
return offset < gapBeginOffset ? buffer[offset] : buffer[offset + GapLength];
}
public string GetText(int offset, int length)
{
int end = offset + length;
if (end < gapBeginOffset)
{
return new string(buffer, offset, length);
}
if (offset > gapBeginOffset)
{
return new string(buffer, offset + GapLength, length);
}
int block1Size = gapBeginOffset - offset;
int block2Size = end - gapBeginOffset;
StringBuilder buf = new StringBuilder(block1Size + block2Size);
buf.Append(buffer, offset, block1Size);
buf.Append(buffer, gapEndOffset, block2Size);
return buf.ToString();
}
public void Insert(int offset, string text)
{
Replace(offset, 0, text);
}
public void Remove(int offset, int length)
{
Replace(offset, length, String.Empty);
}
public void Replace(int offset, int length, string text)
{
if (text == null)
{
text = String.Empty;
}
// Math.Max is used so that if we need to resize the array
// the new array has enough space for all old chars
PlaceGap(offset + length, Math.Max(text.Length - length, 0));
text.CopyTo(0, buffer, offset, text.Length);
gapBeginOffset += text.Length - length;
}
void PlaceGap(int offset, int length)
{
int deltaLength = GapLength - length;
// if the gap has the right length, move the chars between offset and gap
if (minGapLength <= deltaLength && deltaLength <= maxGapLength)
{
int delta = gapBeginOffset - offset;
// check if the gap is already in place
if (offset == gapBeginOffset)
{
return;
}
else if (offset < gapBeginOffset)
{
int gapLength = gapEndOffset - gapBeginOffset;
Array.Copy(buffer, offset, buffer, offset + gapLength, delta);
}
else
{ //offset > gapBeginOffset
Array.Copy(buffer, gapEndOffset, buffer, gapBeginOffset, -delta);
}
gapBeginOffset -= delta;
gapEndOffset -= delta;
return;
}
// the gap has not the right length so
// create new Buffer with new size and copy
int oldLength = GapLength;
int newLength = maxGapLength + length;
int newGapEndOffset = offset + newLength;
char[] newBuffer = new char[buffer.Length + newLength - oldLength];
if (oldLength == 0)
{
Array.Copy(buffer, 0, newBuffer, 0, offset);
Array.Copy(buffer, offset, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
}
else if (offset < gapBeginOffset)
{
int delta = gapBeginOffset - offset;
Array.Copy(buffer, 0, newBuffer, 0, offset);
Array.Copy(buffer, offset, newBuffer, newGapEndOffset, delta);
Array.Copy(buffer, gapEndOffset, newBuffer, newGapEndOffset + delta, buffer.Length - gapEndOffset);
}
else
{
int delta = offset - gapBeginOffset;
Array.Copy(buffer, 0, newBuffer, 0, gapBeginOffset);
Array.Copy(buffer, gapEndOffset, newBuffer, gapBeginOffset, delta);
Array.Copy(buffer, gapEndOffset + delta, newBuffer, newGapEndOffset, newBuffer.Length - newGapEndOffset);
}
buffer = newBuffer;
gapBeginOffset = offset;
gapEndOffset = newGapEndOffset;
}
}
}