More granular UserEvent categorization for transactions

Problem

For the purposes of an editor extension I’m writing (specifically, a transactionFilter), I need to know the direction a transaction was applied in, and whether it was applied as a group operation.

In some operations, this information is already contained within the userevent; e.g.:
Backspace → Userevent: delete.backward

However, most other operations do not:
Backspace (selection) → Userevent: delete.selection    (expecting delete.selection and delete.backward)
Ctrl + Arrow Left → Userevent: select    (expecting select.forward and select.group)

Example usage

Below code would work properly if I do a regular delete (via Backspace/Delete), but not if I have made a selection.

const extension = EditorState.transactionFilter.of(tr => {
    if (tr.isUserEvent('select.forward') && /* OTHER CONDITIONS */) {
         // Adapt transaction selection range to account for specifc case
    }
});
// Register extension ...

Current Situation

This situation wouldn’t be too difficult to resolve, were it not for the fact that the standardKeymap keybinds are already assigned to the editor as an extension – I am unable to change this.

For now, I’ve bodged around this issue by redefining/overwriting all the relevant commands in @codemirror/commands and adding a custom userevent annotation to each of them (to indicate direction and group-ness).

Example overwrite

Only the setSel(...) has been changed; a forward parameter has been added to the function, based on which the select.forward/select.backward annotation would be added.

function setSel(state: EditorState, selection: EditorSelection | {anchor: number, head?: number}, forward?: boolean) {
	let annotations = undefined;
	if (forward !== undefined) {
		annotations = [Transaction.userEvent.of(forward ? "select.forward" : "select.backward")]
	}
	return state.update({selection, scrollIntoView: true, userEvent: "select", annotations})
}

function moveSel({state, dispatch}: CommandTarget, how: (range: SelectionRange) => SelectionRange, forward?: boolean): boolean {
	const selection = updateSel(state.selection, how)
	if (selection.eq(state.selection)) return false
	dispatch(setSel(state, selection, forward))
	return true
}

function cursorByChar(view: EditorView, forward: boolean) {
	return moveSel(view, range => range.empty ? view.moveByChar(range, forward) : rangeEnd(range, forward), forward)
}


export const cursorCharLeft: Command = view => cursorByChar(view, !ltrAtCursor(view))

 

However, there are several (huge) problems with this approach:

  1. To define all these events, I need to re-implement every keybind and their code for my extension.
  2. Furthermore, I’d also have to add this behaviour for all other keybind extensions (emacs/vim) – though this is not necessarily the highest priority.
  3. Some of the keybinds can not be overwritten: specifically all the arrow up/down keybinds. Is this due to the fact that they do not have the preventDefault parameter set to true (compared to ArrowLeft/Right)?

(Preferred) Solution

In a perfect world, I’d want to get all the necessary context of the action taken within the transaction itself. However, I realise that is probably not very feasible.

In all honesty, the solution I have currently, is not too bad – it does what I need it to do. However, the only dealbreaker is those keybinds that I cannot replace (see [3] above).


Any advice would be incredibly appreciated.

Many thanks in advance!

All keybindings can be overwritten with higher-precedence bindings (or by leaving them out of the configuration altogether). If you are sure it’s not working like that, please set up a minimal script that clearly demonstrates the problem.

1 Like

Many thanks for your quick and insightful response. I did not realize that the precedence rules could also be applied here.

Prec.high(...) didn’t seem to be sufficient to override the original binds, but Prec.highest(...) did. Everything seems to behave correctly now.

Thanks again for your help, I really appreciate it.