追踪变更
Tracking changes
变更(changes)在 ProseMirror 中是一等公民。你可以持有它们,并对它们做各种事情。例如把它们rebase到其他变更之上、反转它们,或者检查它们看看它们做了什么。
Changes are first-class values in ProseMirror. You can hold on to them, and do things with them. Such as rebasing them across other changes, inverting them, or inspecting them to see what they did.
这个示例利用这些特性,让你能够「提交」你的变更、撤销单个提交,以及找出某段文本源自哪个提交。
This example uses those properties to allow you to “commit” your changes, to revert individual commits, and to find out which commit a piece of text originates from.
把鼠标悬停在提交上,可以高亮它们引入的文本。
Hover over commits to highlight the text they introduced.
这个页面不会列出示例的全部源代码,只列出最有趣的部分。
This page won't list the whole source code for the example, only the most interesting parts.
我们首先需要一种跟踪提交历史的方式。一个编辑器插件很适合做这件事,因为它可以在变更到来时观察它们。这是插件状态值的样子:
The first thing we need is a way to track the commit history. An editor plugin works well for this, since it can observe changes as they come in. This is what the plugin's state value looks like:
class TrackState {
constructor(blameMap, commits, uncommittedSteps, uncommittedMaps) {
// The blame map is a data structure that lists a sequence of
// document ranges, along with the commit that inserted them. This
// can be used to, for example, highlight the part of the document
// that was inserted by a commit.
this.blameMap = blameMap
// The commit history, as an array of objects.
this.commits = commits
// Inverted steps and their maps corresponding to the changes that
// have been made since the last commit.
this.uncommittedSteps = uncommittedSteps
this.uncommittedMaps = uncommittedMaps
}
// Apply a transform to this state
applyTransform(transform) {
// Invert the steps in the transaction, to be able to save them in
// the next commit
let inverted =
transform.steps.map((step, i) => step.invert(transform.docs[i]))
let newBlame = updateBlameMap(this.blameMap, transform, this.commits.length)
// Create a new state—since these are part of the editor state, a
// persistent data structure, they must not be mutated.
return new TrackState(newBlame, this.commits,
this.uncommittedSteps.concat(inverted),
this.uncommittedMaps.concat(transform.mapping.maps))
}
// When a transaction is marked as a commit, this is used to put any
// uncommitted steps into a new commit.
applyCommit(message, time) {
if (this.uncommittedSteps.length == 0) return this
let commit = new Commit(message, time, this.uncommittedSteps,
this.uncommittedMaps)
return new TrackState(this.blameMap, this.commits.concat(commit), [], [])
}
}
插件本身所做的,不外乎监视事务并更新它的状态。当事务上带有以该插件为标签的 meta 属性时,它就是一个提交事务,该属性的值就是提交信息。
The plugin itself does little more than watch transactions and update its state. When a meta property tagged by the plugin is present on the transaction, it is a commit transaction, and the property's value is the commit message.
import {Plugin} from "prosemirror-state"
const trackPlugin = new Plugin({
state: {
init(_, instance) {
return new TrackState([new Span(0, instance.doc.content.size, null)], [], [], [])
},
apply(tr, tracked) {
if (tr.docChanged) tracked = tracked.applyTransform(tr)
let commitMessage = tr.getMeta(this)
if (commitMessage) tracked = tracked.applyCommit(commitMessage, new Date(tr.time))
return tracked
}
}
})
像这样跟踪历史能支持各种有用的功能,比如弄清楚某段代码是谁在什么时候添加的,或者撤销单个提交。
Tracking history like this allows for all kinds of useful things, such as figuring out who added a given piece of code, and when. Or reverting individual commits.
撤销旧的 step 需要把这些 step 的反转形式rebase到所有中间 step 之上。这个函数做的就是这件事。
Reverting an old steps requires rebasing the inverted form of those steps over all intermediate steps. That is what this function does.
import {Mapping} from "prosemirror-transform"
function revertCommit(commit) {
let trackState = trackPlugin.getState(state)
let index = trackState.commits.indexOf(commit)
// If this commit is not in the history, we can't revert it
if (index == -1) return
// Reverting is only possible if there are no uncommitted changes
if (trackState.uncommittedSteps.length)
return alert("Commit your changes first!")
// This is the mapping from the document as it was at the start of
// the commit to the current document.
let remap = new Mapping(trackState.commits.slice(index)
.reduce((maps, c) => maps.concat(c.maps), []))
let tr = state.tr
// Build up a transaction that includes all (inverted) steps in this
// commit, rebased to the current document. They have to be applied
// in reverse order.
for (let i = commit.steps.length - 1; i >= 0; i--) {
// The mapping is sliced to not include maps for this step and the
// ones before it.
let remapped = commit.steps[i].map(remap.slice(i + 1))
if (!remapped) continue
let result = tr.maybeStep(remapped)
// If the step can be applied, add its map to our mapping
// pipeline, so that subsequent steps are mapped over it.
if (result.doc) remap.appendMap(remapped.getMap(), i)
}
// Add a commit message and dispatch.
if (tr.docChanged)
dispatch(tr.setMeta(trackPlugin, `Revert '${commit.message}'`))
}
由于在把变更相互移动时存在隐式的冲突解决,复杂撤销(即后来的变更触碰到相同内容时)的结果有时可能不太直观。在生产应用中,检测这类冲突并为用户提供一个解决冲突的界面可能是可取的。
Due to the implicit conflict resolution when moving changes across each other, outcomes of complicated reverts, where later changes touch the same content, can sometimes be unintuitive. In a production application, it may be desirable to detect such conflicts and provide the user with an interface to resolve them.