Refactor Commands into Transform Helpers

prosemirror-commands provides many very helpful commands for joining/lifting/etc. In the process of building complex commands, I find myself wanting to use these builtin commands as building blocks. Unfortunately their API is to take state and onAction and actually apply the changes. I propose instead to refactor these commands into methods that operate on a transform and returns either false (could not execute command) or the new transform. This would allow for easier chaining of builtin commands in creating custom commands.

As an example of one of these custom commands, I have a command that will lift all blocks within selection and then wrap selection in provided block type. This works by operating on a transform to lift all blocks and then calling wrapIn on the transform.selection. Iā€™d like to call the wrapIn provided but that takes a state and I need to operate on my transform which has everything lifted. With this command I can turn a selection into a blockquote and remove bullets & headings.

Thoughts?

5 Likes

Note that while this definitely holds for some of the stuff that the commands package does, joining and lifting, specifically, are based on primitives from the transform package which are exported.

This, for example, really shouldnā€™t be hard to write on top of liftTarget, Transform.lift, and Transform.setBlockType.

Returning a transform from a command isnā€™t going to work, because commands, as they work now, can create any action, not just transformations. What you could do, in some cases, is pass a custom onAction callback to a recursively called command, but thatā€™s not super straightforward.

The idea is that commands are interface units that you bind to keys or menu items, which do one thing and perform all the checking of whether that thing makes sense, and for that the current interface works well. Utilities to be used by commands are a separate thing, and Iā€™m open to factoring some of those into a more reusable form.

Absolutely. Iā€™m not suggesting the commands interface changes. More like, the commands that exist now would be wrappers around transform utility functions.

Does the introduction of Transaction change the landscape enough for this to be reconsidered? I can appreciate that commands are more powerful than transforms, because transforms donā€™t encapsulate all the state thatā€™s in play (e.g. stored marks), but from what Iā€™ve gathered so far transactions do?

Ultimately I think my use-case is the same as @wes-r ā€” I want composable mutations. I think I want this so that I have full control over where history events (by treating each transaction as a history event, and being responsible for building up a single transaction of appropriate changes before dispatching it).

What I have in mind is an interface like this:

interface Mutator {
    // Apply the mutation to a transaction, returning the resulting transaction.
    // If the mutation can't be applied, returning the original transaction unchanged.
    apply(tr: Transaction): Transaction;
    // Return true if the mutation can be applied.
    canApply(tr: Transaction): boolean;
}

Itā€™s very similar to a command as they exist today, but it doesnā€™t know anything about the view and dispatching.

1 Like

The command interface isnā€™t going to be overhauled again, for the sake finally stabilizing but also because I think their current form expresses what they do better (they may have side effects, beyond dispatching a transaction).

You can extend commands with this slightly awkward but unproblematic pattern:

function andSelectAll(command) {
  return (state, dispatch) => {
    return command(state, dispatch && (tr => {
      if (tr.isGeneric) // Don't touch non-generic transactions
        tr.setSelection(Selection.between(tr.doc.resolve(0),
                                          tr.doc.resolve(tr.doc.content.size)))
      dispatch(tr)
    }))
  }
}

[Edit: Actually balance brackets]

1 Like

Iā€™m happy for commands to stay as they are, but what Iā€™d propose is decomposing the implementation into ā€œmutatorsā€ (open to a better name) that are composable. The end result should be commands that maintain their current interface, but whose internals are probably just a couple of lines combining mutators and calling dispatch.

I think Iā€™m missing something regarding the difference between transactions and editor state, specifically regarding what state they encapsulate. Whatā€™s missing from transactions that prevent commands from only working with transactions? Itā€™s surprising to me that itā€™s not an explicit goal for commands to work on transactions, so I believe thereā€™s some consideration that Iā€™m overlooking.

I suspect we made a mistake with this thread in conflating transforms with commands ā€” commands can continue to live as-is, but (I think what @wes-r and) I want is something new thatā€™s similar to commands, but is a lower-level composable construct. In practice Iā€™d probably end up never using commands, and always build my own because of specific user-experience requirements that Iā€™m aiming for.

1 Like

Having two functions, one to check whether something applies and one to execute it, is how things initially worked but it was really hard to keep those two consistent, so I moved to the one-function-with-optional-dispatch form, and it made the code much simpler and less error prone.

Adding another concept of helpers that implement the functionality of the commands is possible, but Iā€™m not really convinced the added value is worth the extra API surface.

FWIW I came across this thread as I was running into the exact same problem - Iā€™ve been using the built in commands, and theyā€™re great up until I need something slightly different, or I need to compose a few of them together, but ultimately Iā€™m having to just copy the internals of them out of prosemirror-commands and reimplement locally.

@bradleyayers idea of a lower level composable interface would be ideal for me, and I think Iā€™m in a similar boat - I doubt Iā€™d end up actually using any of the built in commands, as nearly all the behaviours Iā€™m after are just slightly different to the built in commands.

For me, I think itā€™s worthwhile to have the extra API surface - IMO I donā€™t think it would be too much cognitive overhead for people coming to ProseMirror, and it would be incredibly worthwhile. I found it really confusing that I couldnā€™t chain together commands (until I properly understood their inner workings) - indeed thatā€™s what I thought the chainCommands helper initially did. Having a different set of ā€˜utilitiesā€™ would help clear this up I think.

4 Likes

Ping @marijn - I hit this again! The liftListItem implementation in prosemirror-schema-list is awesome as it will do the right thing for any nested list items. The issue I ran into is wanting to invoke it several times to lift an item ALL THE WAY out of the list (a ā€œremoveā€ command). Presently there is no good way to do this. I had to copy the implementation and modify it to operate on a transaction so that I can invoke it in a loop.

All of this great functionality is locked away behind dispatched commands. Instead they should be building blocks! Seems many of us long term users have solved this by copying the innards of the commands we want to build upon. This is not good! It makes managing our code harder and it adds a barrier to entry for beginners as they will inevitably run into the same desire and the same less-than-perfect-solution.

35%20AM ā€”> 48%20AM

1 Like

I definitely agree that there are a few utility-type functions that Iā€™ve either copied and pasted from the core lib or written from scratch and subsequently realised that Iā€™d reimplemented something in that was already in the core library.

1 Like

Also ā€“ thereā€™s no reason to drastically increase the API surface area. If all of the existing commands operated on transactions and returned one, then there could be a new high level command helper dispatcher() or something like that, so dispatcher(joinBackward()) would be equivalent to the joinBackward that exists today.

You usually donā€™t want the exact command as a helper, though. Where a command will act on the selection, a helper tends to work better when it takes arguments that tell it where to apply its effect.

But Iā€™m okay with growing the API surface a little to expose reusable code where itā€™s appropriate. Do you want to propose a pull request that splits the guts of liftListItem into an exported utility function?

YES! Iā€™ll hopefully have something for you next week.

Can you elaborate on this? or provide an example of the signature youā€™d like to see for this helper? Iā€™m currently using:

export function liftListItem(itemType) {
  return function(selection, tr) {
    ...

I have been exploring the idea of using transforms for a while and it seems to be working quite well for us. Here are a few examples: https://github.com/atlassian/prosemirror-utils/blob/master/src/transforms.js

I was going to decompose PM commands into utils in a similar way as you described, I just didnā€™t have enough time so far to do that :slight_smile:

Passing a selection makes sense. Or it could just be a pair of positions. Also, Iā€™d include itemType directly in the parameters of the functionā€”if we donā€™t have to correspond to the command function signature, thereā€™s not need for the wrapping factory function.

I also run into this issue now and then, wanting to combine the functionality of several commands. However, that use case doesnā€™t seem to be supported since the commands immediately dispatch the transactions and Iā€™m left with copy-pasting the code from prosemirror-commands.

Iā€™ll try employing the pattern suggested by @marijn, but I feel that the API could be improved here to avoid code duplication. I donā€™t think this discussion has lead to changes in the code, has it?

This doesnā€™t allow calling commands inside the arrow function, does it, since they require state to be passed in?

I donā€™t know the design decisions behind the current API, but Iā€™ll take the liberty to present the following, possibly foolish suggestion. :slight_smile:

Looking at the prosemirror-commands source code, commands only use the state to get the current document, selection and storedMarks. I believe these can also be retrieved from a transaction and the interface of commands could possibly be changed to accept only a transaction instead of the editor state?

Considering if (dispatch) dispatch(...) is (almost) always followed by return true, perhaps commands can return a transaction when the command can be applied, or null otherwise (equivalent to a return value of false now). This does however mean that transactions are constructed, even if they are not going to be dispatched by the caller. Is that too wasteful?

This kind of interface allows chaining commands, AFAICS. Instead of changing the commands interface, an intermediate level of mutators can be introduced, as suggested by @bradleyayers. Backwards-compatible commands could be constructed automatically from these:

function createCommand(mutator) {
  return function(state, dispatch) {
    let tr = mutator(state.tr)
    if (tr === null)
      return false
    dispatch(tr)
    return true
  }
}

That may go for the commands in that package, but is not generally true. For example, the undo/redo commands will access the stateā€™s history field.

As I pointed out in other messages, I donā€™t think it is possible to compose commands, in general, and having use-case-specific helper functions that implement the difficult parts some commands would be the way to go.

In case anyone is still interested in this topic I wrote a blog post about how composable commands are being accomplished in remirror.

The main idea is to provide a transaction that is shared for the commands that should be composed together.

Here is the function that can be used to create a pseudo state object which uses the same transaction for each call to the state.tr.

function chainableEditorState(tr: Transaction, state: EditorState): EditorState {
  return {
    ...state,
    tr,
    schema: state.schema,
    plugins: state.plugins,
    apply: state.apply.bind(state),
    applyTransaction: state.applyTransaction.bind(state),
    reconfigure: state.reconfigure.bind(state),
    toJSON: state.toJSON.bind(state),
    get storedMarks() {
      return tr.storedMarks;
    },
    get selection() {
      return tr.selection;
    },
    get doc() {
      return tr.doc;
    },
  };
}

You can pass this state to multiple commands with an empty dispatch function and then dispatch the tr so that all the updates are composed onto the same transaction.

1 Like