Tracking changes
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) {
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), [], [])
}
}
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.
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 (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}'`))
}
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.