从线性撤消历史记录构造撤消树

2021-03-21 05:28:37

Emacs带来了一个强大但可观的奇怪的撤消系统,它考虑了撤消自己撤消的动作,所以而不是重做,你只是撤消以前的撤消。这允许您返回每个先前的缓冲状态,传统的撤消系统无法保证。但是,当你撤消时,Emacs的撤消历史可以很容易地摆脱手,然后撤消该撤消,然后撤消撤消的撤消......你很快就丢失了撤消历史记录的心理模型,并最终保持撤消按钮,直到看到所需的缓冲区状态。

一个想法利用两个世界都是使用撤消树。撤消树易于导航和理解。无处不在的undo-tree.el正是为此。作为一个编码挑战,我一直在考虑如何从线性撤消记录中构建树。这样,我们可以避免保持撤消树的内部数据结构。这篇文章描述了我弄清楚的方式。

;; “缓冲区撤消列表”' (NIL(11369 11377);从11369插入113777.nil(11332。11344);从11332到11344插入。("< li" 11332);"< Li"到11322.nil(t 24653 33109 208947 953000))

EMACS在缓冲区撤消列表中保留撤消记录。每次缓冲内容更改时,EMAC都会将多个条目推到列表上,每个条目都表示插入,删除等更改。这些条目由NIL条目分组为分隔符,因此可以立即撤消多个操作。在这里,我将调用一组条目“修改”。

例如,如果我将“abcde”插入缓冲区1,并且撤消两次,我将在缓冲区中看到“abc”,并且将推到两个撤消修改,然后将两个用于删除“e”的缓冲区 - undo-list。另一个“D”。如果我们继续撤消,我们将继续返回,直到所有编辑都已撤消。

为了停止撤消并开始重做,我们按下撤消撤消链的C-G。我们刚刚创建的所有撤消记录现在被认为是普通修改,并进一步撤消这些以前的撤消。所以,如果我们撤消两次,我们就会恢复到“ABCDE”。

这个“撤消链”的工作如何?当我们调用第一个undo命令时,它将待定 - 撤消列表设置为缓冲区撤消列表的值;进一步撤消命令从挂起 - 撤消列表中弹出修改并扩展缓冲区撤消列表。当我们打破撤消链时,下一个undo命令将再次将待定 - 撤消列表设置为缓冲区撤消列表的值。

以下是在分支发生时发生的事情:如果我们撤消到“ABC”,并插入“F”。 EMACS只需将新的修改推动到缓冲区撤消列表“之前。

除了扩展缓冲区撤消列表外,EMAC还将缓冲区状态映射到它们的等同物。 Emacs未知修改后,它将缓冲区 - 撤消列表的尖端映射到undo-Equiv-table中的撤消撤消列表提示。这是从线性撤消列表构造树的关键。

BTW,如图所示,撤消撤消列表和缓冲区撤消列表实际指向同一列表对象,即该列表中的不同CONS单元格。

以下是一个示例撤消树和相应的撤消列表。撤消列表可以被视为“包裹”树。要从撤消列表中“构造”树,我们需要知道:

要显示哪个节点。在列表中,节点5和6是早期节点的重复项,不需要出现在树中。

在节点之间建立父子关系。从节点0开始,它需要知道节点1是它的孩子,节点1需要知道2是它的孩子,等等。

在undo-equvi-table的帮助下很容易弄清楚:对于修改m(创建缓冲状态m的修改m),如果它是普通的变化,则缓冲状态m必须是m-1的一个孩子;如果m是撤消更改,则缓冲状态m必须等同于上一个节点,例如,n。 m相当于n意味着1)我们不在树中画m,只有n和2)m的孩子是n的孩子。

在这个例子中,2是普通的修改,所以2是1. 6的孩子是撤消修改,相当于2,所以我们不绘制6,只有2;和6个孩子,7,成为2个孩子。

所以它结果,构造树很简单:我们首先转过缓冲区 - 撤消列表以生成修改列表。然后我们浏览修改列表,识别等效节点并建立父子关系。最后,我们可以从第一个节点开始绘制树,深度第一或宽度。

绘制树只有一半的故事,如果我们不能在树上移动,那么撤消树就不能及时地走向。假设我们在节点m并希望移动到节点n。 Emacs应该让我们回来什么?

我的直接思想是刚刚在我们处于节点n之前反复调用撤消。它有效,但只有在10码范围内:简单的动作很容易爆炸撤消列表。例如,假设我们处于节点1并返回到节点0,撤消列表会发生什么?

我们需要从9次撤消回到0,前回到1到3之间。更糟糕,如果我们现在想要从0到1,我们需要从18到1撤消。撤消列表每次都会加倍我们来回移动到0到1之间。

嗯,如果我们处于撤消链,从1到0撤消将是如此简单:撤消撤消列表将指向1(而不是9),我们只需撤消修改1.为什么我们不做这是,无论我们是在撤消链吗?在链接期间,撤消从撤消撤消列表中的修改并将其送入原始撤消。我们可以类似地找到0到1之间的修改,并将其归因于原始撤消。

让我们更仔细地看看它。如果我想从缓冲状态1到2,如何找到馈送到原始撤消的修改列表?我们可以将其馈送到5-4,或修改9到9-8,两者都将我们返回到节点2的缓冲状态。或者我们甚至可以去9-8-7-6-5- 4,或5-4-3-2。

因此,要从M到N找到有效的“路由”,它需要满足:1)开始等同于M,目的地等同于n,2)开始比目的地较大,即,启动&gt ;结束,因为原始 - 撤消只能在撤消列表中倒退。一旦我们找到了所有有效的路线,我们都会选择最短的路线并将其喂给原始撤消,传送!

;;简化的“vundo-m' (cl-defstruct vundo-m ;;作为mod列表中的修改:idx undo-list ;;一个双链接的等价状态列表:prev-eqv next-eqv ;;作为树中的节点:儿童父点)

github和本地备份。该包装需要Emacs的当前开发分支,即Emacs 28.程序的流程大致:

启动vundo - 刷新缓冲区中的进程。它确定我们是否正在从头开始生成所有内容,或逐步更新我们的数据。

每个修改都存储在vundo-m struct中,它还表示相应的缓冲状态和树中的相应节点。

1通常在插入“ABCDE”时,单个更改将分配为1。在这里,为了演示,我们假设每个插入都会创建一个单独的记录。

3虽然未释放CONS单元格,但缓冲区撤消列表确实会缩小。这很好,因为我们所需的所有信息都存储在我们的数据结构中,这不会改变。