追踪变更

Tracking changes

变更(changes)在 ProseMirror 中是一等公民。你可以持有它们,并对它们做各种事情。例如把它们rebase到其他变更之上、反转它们,或者检查它们看看它们做了什么。

这个示例利用这些特性,让你能够「提交」你的变更、撤销单个提交,以及找出某段文本源自哪个提交。

Commit message:

把鼠标悬停在提交上,可以高亮它们引入的文本。

这个页面不会列出示例的全部源代码,只列出最有趣的部分。

我们首先需要一种跟踪提交历史的方式。一个编辑器插件很适合做这件事,因为它可以在变更到来时观察它们。这是插件状态值的样子:

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 属性时,它就是一个提交事务,该属性的值就是提交信息。

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
    }
  }
})

像这样跟踪历史能支持各种有用的功能,比如弄清楚某段代码是谁在什么时候添加的,或者撤销单个提交。

撤销旧的 step 需要把这些 step 的反转形式rebase到所有中间 step 之上。这个函数做的就是这件事。

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}'`))
}

由于在把变更相互移动时存在隐式的冲突解决,复杂撤销(即后来的变更触碰到相同内容时)的结果有时可能不太直观。在生产应用中,检测这类冲突并为用户提供一个解决冲突的界面可能是可取的。