Tracking changes

变化在ProseMirror中是一级值。你可以保留它们,并对它们进行操作。例如重新基准化它们,反转它们,或检查它们以查看它们的作用。

此示例使用这些属性允许您“提交”您的更改,恢复单个提交,并找出某段文本的提交来源。

译者注:下方编辑器内容为 js 生成,因此不翻译其内的内容
Commit message:

将鼠标悬停在提交上以突出显示它们引入的文本。

此页面不会列出完整的源代码,仅列出最有趣的部分。

我们首先需要的是一种跟踪提交历史的方法。一个编辑器插件对此非常有效,因为它可以观察到变化的到来。这是插件的状态值的样子:

class TrackState {
  constructor(blameMap, commits, uncommittedSteps, uncommittedMaps) {
    // 责任图是一个数据结构,列出了一系列
    // 文档范围,以及插入它们的提交。
    // 可以用于,例如,突出文档的部分
    // 那是由一次提交插入的。
    this.blameMap = blameMap
    // 提交历史,作为一个对象数组。
    this.commits = commits
    // 反向步骤及其对应于更改的映射
    // 自上次提交以来已做出。
    this.uncommittedSteps = uncommittedSteps
    this.uncommittedMaps = uncommittedMaps
  }

  // 将此状态应用转换
  applyTransform(transform) {
    // 反转交易中的步骤,以便能够保存它们
    // 下一个提交
    let inverted =
      transform.steps.map((step, i) => step.invert(transform.docs[i]))
    let newBlame = updateBlameMap(this.blameMap, transform, this.commits.length)
    // 创建一个新状态——因为这些是编辑器状态的一部分,
    // 持久化数据结构,它们不能被改变。
    return new TrackState(newBlame, this.commits,
                          this.uncommittedSteps.concat(inverted),
                          this.uncommittedMaps.concat(transform.mapping.maps))
  }

  // 当事务被标记为提交时,这用于放置任何
  // 未提交的步骤进入一个新的提交。
  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), [], [])
  }
}

插件本身除了监视事务和更新其状态外,几乎没有其他作用。当事务中存在由插件标记的元属性时,它是一个提交事务,并且该属性的值是提交消息。

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

跟踪这样的历史可以实现各种有用的功能,例如找出是谁在何时添加了某段代码,或者还原单个提交。

恢复旧步骤需要将这些步骤的反向形式变基到所有中间步骤之上。这就是这个函数的作用。

import {Mapping} from "prosemirror-transform"

function revertCommit(commit) {
  let trackState = trackPlugin.getState(state)
  let index = trackState.commits.indexOf(commit)
  // 如果此提交不在历史记录中,我们无法还原它
  if (index == -1) return

  // 只有在没有未提交的更改时才能恢复
  if (trackState.uncommittedSteps.length)
    return alert("Commit your changes first!")

  // 这是文档开始时的映射
  // 提交到当前文档。
  let remap = new Mapping(trackState.commits.slice(index)
                          .reduce((maps, c) => maps.concat(c.maps), []))
  let tr = state.tr
  // 构建一个包含所有(反向)步骤的事务
  // 提交,已重新基于当前文档。他们必须被应用
  // 反向顺序。
  for (let i = commit.steps.length - 1; i >= 0; i--) {
    // 映射被切片以不包括此步骤的映射和
    // 之前的那些。
    let remapped = commit.steps[i].map(remap.slice(i + 1))
    if (!remapped) continue
    let result = tr.maybeStep(remapped)
    // 如果可以应用该步骤,请将其映射添加到我们的映射中
    // 管道,以便后续步骤映射到其上。
    if (result.doc) remap.appendMap(remapped.getMap(), i)
  }
  // 添加提交信息并分发。
  if (tr.docChanged)
    dispatch(tr.setMeta(trackPlugin, `Revert '${commit.message}'`))
}

由于在移动更改时隐式解决冲突,当后续更改触及相同内容时,复杂的还原结果有时可能不直观。在生产应用中,可能需要检测此类冲突并为用户提供解决这些冲突的界面。