-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Propose history tracers #12
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be super useful!
|
||
- Include such a step in your transaction, and associate the tracer with it. | ||
|
||
This is slightly awkward, but works robustly. We can consider including a no-op step type in the prosemirror-transform package to make it easier. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case it's informative, our team at the New York Times has already done exactly this, and it has worked well for exactly these non-mutating use-cases you describe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's helpful to know, thanks.
|
||
When you create a transaction that contains some traceable steps, you use the `tracers` metadata key to add this information to the transaction. It holds an array of `Tracer` objects, each of which has an index that points at a step in the transaction, a tag that identifies the kind of tracer it is, and optionally an additional value of arbitrary type that provides further information. | ||
|
||
The undo history stores these tracers along with the steps, and When it creates a transaction that undoes or redoes some steps, it includes the relevant tracers in that transaction. An `event` property on tracers can be used to see what kind of transaction this is—it initially holds `"do"` in the user-created transaction, `"undo"` when the transaction undoes the step, and `"redo"` when it redoes it again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't part of the scope of this RFC as it stands, but I think Tracer
seems like it could also be a nice fit for the rebase operation that happens in prosemirror-collab as well; they both mainly concern themselves with steps, and both can have the effect of replaying steps that may or may not have trace-able data in them. Just a thought!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, that's a good point. Just to check if we're on the same page, at a minimum this would involve:
-
Moving tracers into a more central package, probably prosemirror-state
-
Having the collab module add tracers with a type like "rebase" when it rebases traced steps
Can you sketch a situation in which such information would be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes....some pseudo-code for a hypothetical transaction utility function that wanted to consume/produce such things:
import { tracers } from 'prosemirror-tracer'
function wrapWithMetadata(tr, userId, isRebaseable) {
let tracers = tr.getMeta(tracers)
let traceData = []
tr.steps.forEach((step, i) => {
traceData.push(new Tracer(i, "stepMetaData", {
userId,
isRebaseable,
createdAt: Date.now()
}))
});
if (!tracers) {
tr.setMeta(tracers, traceData))
} else {
tracers.forEach(trace => {
// we copy the trace data for collab rebases
// and redoes/undoes UNLESS the tracer metadata
// specifically designates us not to
if (!tr.steps[trace.index] instanceof NoOpStep || !trace.value.isRebaseable || ['undo'].includes(trace.event)) {
traceData[trace.index].value = trace.value;
}
})
}
myCustomBackend
.send(traceData)
.then(() => console.warn('per-transaction metadata sent'))
}
In this particular implementation, the idea is that depending on some combination of the trace data itself, and the union type of the event (which in my thinking could come from prosemirror-collab
vs prosemirror-history
....not sure the best way to represent this), determines whether or not we copy the trace data that came from the original transaction, vs. using the "fresh" trace data.
I'm not totally sure the best way to represent the event
to allow for both history/collab...maybe this could also be namespaced to the plugin that ends up copying/preserving the trace data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Would it make sense for the collab module, when it rebases steps, to add tracers for those steps twice—once when it reverts them at the start of the rebase, and once when it re-applies them at the end?
I think, since the number of different actions that involve re-applying past steps is quite limited, we should be fine with just using strings for the event types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this revision look to you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks good; so for the average rebase, where a step isn't dropped, you'd end up with two traces — one for rebase-invert
and rebase-reapply
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. That's slightly awkward, in that you have to check whether both or only one tracer is present for a step to determine what happened to it, but it seems the most straightforward way to describe what's going on.
My use-case for this would be tracking ranges over a document. Presumably I'd store the range information in plugin state, and then update when the plugin is given the transaction. I think there should be enough information available through tracers to do this. Having a |
What kind of ranges, and how do individual steps come into that? Is this something like adding a comment/annotation, and being able to undo and redo that? |
Following discussion with @saranrapjs , I've updated the proposal to increase its scope a bit—the collab module could also use tracers to provide more information about rebased steps. |
@marijnh just FYI my particular use case looks similar to what you described. Comments plugin is implemented using decorations. Once a user deletes a text with a comment decorations and then undo deletion - text is restored while comments are lost. |
I see. That case wouldn't be straightforwardly covered by this feature, unfortunately—the steps that delete the commented text are likely created by code that doesn't know about comments, and thus won't be adding the tracers to indicate that a given step deletes a comment (which you could then use to restore it when the step is undone). That is a common use case though, and we should probably try to incorporate it in the proposal. It seems like it would be necessary for plugins to somehow add tracers to any transaction that comes in, maybe through a hook that's called right after the transaction filter. That seems a bit random, and I don't have time today to think about it more deeply, but I guess it should be safe (adding trace metadata shouldn't conflict with any other functionality). |
Patch 6a69d90 adds a plugin hook |
@@ -18,6 +18,8 @@ When you create a transaction that contains some traceable steps, you use the `t | |||
|
|||
Both the `Tracer` class and the `tracers` metadata key are exported from prosemirror-state. | |||
|
|||
To cover the use case where tracers need to be attached to steps by code that didn't originate those steps, for example when you want undoing the deletion of content that had some metadata associated with it to restore that metadata, plugins support a new hook, `attachTracers`, which is called right after `filterTransaction`, and given a transaction may return an array of additional tracer objects to associate with the transaction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear: when undoing steps that have tracers associated with them, the existing spec already will re-add tracers, but this new capability is to support cases where the metadata may be specific to the content of the steps? Is attachTracers
implemented somewhere we can see? (mostly just trying to get a sense for whether we need this vs. if just attaching tracers when dispatching the transaction will suffice)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this hasn't been implemented yet. But basically, the transaction apply driver will, before applying each transaction, see if any plugins want to add tracers, and if so, add them to the transaction's existing set of tracers. So any downstream code (such as state field apply methods) will see the full set of tracers.
That does mean that attachTracer
callbacks may in some cases have to check whether a tracer already exists—for example if you delete a range that had a comment associated with it, a plugin might add information about that comment as a tracer. If that step is then undone and redone, the plugin will see the same step again deleting a comment, but since it still has that initial tracer, you probably don't want to add another instance of it. That's awkward, but, I guess, workable.
Has anybody found the time yet to try to build a proof-of-concept of their desired feature on top of this? I'd love to have some feedback from someone who's tried to actually use it before I finalize this. |
Hey @marijnh we've been swamped but I think we can find time to do this early next week |
Unfortunately I haven't either. |
Any update on this functionality? Would be super useful for tracking comments properly |
Given that I never received concrete feedback, and that the proposal feels a bit heavyweight and awkward, I didn't proceed with this. I've recently been working on the same problem in CodeMirror version 6, where it ended up working quite differently. My current thinking is that the ideas in this RFC aren't the way to go. An alternative approach using custom step types might work better, but in order to track comment deletion through content deletion you'd still need to kludge something I guess the summary is that this needs a lot more work, which I'm unlikely to put in on the short term. |
This would be really nice if it would enable us to preserve the proper decorations upon undo. Currently I can't see a way to restore the decorations when undoing. @marijnh is there any chance this will be revisited? We're using TipTap so it's probably not plausible for us to try the old branches you mentioned above but I'd be happy to test it out otherwise. |
This proposal adds features to the prosemirror-history package that allow you to see when a given step is undone or redone.
An initial implementation exists in this branch.
Rendered RFC