Decoration.replace side

It would seem that when using Decoration.replace, we have no control over the side of the rendered widget. However, I’m running into a situation where being able to set a value here manually helps:

I have the following text Text1Text2, where Text1 and Text2 are both replaced with widgets (using Decoration.replace). Now I’d like to insert a widget (using Decoration.widget) between the two as follows:

["Text1" = Widget1][Widget2]["Text2" = Widget3].
(Note that Widget2 and Widget3 both start at position 5, but Widget3’s “side” parameter can’t be changed)

Now when I’m building these widgets using a RangeSetBuilder, I get an error because it thinks the ordering should be:
["Text1" = widget 1]["Text 2" = widget 2][Widget]

Is there anything currently in the API that allows setting the side for a replacement widget?

I think it’s much weirder - the replace widget actually eats up the Widget2 in the middle…

The inclusive options control the side. Replace widgets that are inclusive on a given side will cover any regular widgets at that position, whereas non-inclusive replacements won’t.

Hmm I’m using Decoration.replace({}) which should set inclusive to false according to the docs:

Whether this range covers the positions on its sides. This influences whether new content becomes part of the range and whether the cursor can be drawn on its sides. Defaults to false.

I’m not seeing this problem — creating two adjacent replace decorations with a widget decoration between them just works, when I try it.

Seems to only happen to block widgets. Below is a minimal testing case:

  1. Open CodeMirror 6 and open the dev console.
  2. Run the following code and observe that the test widget shows up in DOM (it shows up as a new line)
  3. Reload page and run the same code but uncomment the [1,2] replace, and observe that the DOM element does not show up.
const {EditorState, EditorView, basicSetup} = CM["@codemirror/basic-setup"];
const {WidgetType, Decoration} = CM["@codemirror/view"];
const {StateField} = CM["@codemirror/state"];
class TestWidget extends WidgetType {
	toDOM(view) {
		return document.createElement('div');
	}
}
let field = StateField.define({
	create() {
		return Decoration.none;
	},
	update(decos, tr) {
		let newDecos = [];
		// UN-COMMENT NEXT LINE IN TEST 2
		// newDecos.push(Decoration.replace({}).range(1, 2));
		newDecos.push(Decoration.widget({
			widget: new TestWidget(),
			block: true
		}).range(2));
		newDecos.push(Decoration.replace({}).range(2, 3));

		return Decoration.set(newDecos, true);
	},
	provide: f => EditorView.decorations.from(f)
})
let state = EditorState.create({
	doc: `abcd`,
	extensions: [basicSetup, field]
});
view.setState(state);

Image from step 2
image

Image from step 3
image

Expected
image

There was some rather funky logic in the way block widgets were assigned sides. I am not entirely sure anymore what problem I was trying to solve with that, but in this case it seems problematic—it made it impossible to do what you are trying to do here, by always making the widget move into adjacent replace decoration ranges. This patch simplifies this, and should make this issue go away.

1 Like

I feel really bad for putting you through this, I’m really sorry!

I seem to be continually running into Decoration adjacency issues. Right now I’m having trouble with block replace side-by-side with a block widget.

Same testing method at CodeMirror 6 - paste and run in developer console.

This following code snippet should add a “block replace” widget at [0,4] and then a “block widget” at [4]. However, the block widget does not show up.

Observed behavior:
image

const {EditorState, EditorView, basicSetup} = CM["@codemirror/basic-setup"];
const {WidgetType, Decoration, ViewPlugin} = CM["@codemirror/view"];
const {StateField} = CM["@codemirror/state"];
class TestWidget extends WidgetType {
	toDOM(view) {
        let el = document.createElement('div');
        el.innerHTML = 'Test element';
		return el;
	}
}
let decos = Decoration.set([
  Decoration.replace({widget: new TestWidget(), block: true, side:1}).range(0,4),
  Decoration.widget({widget: new TestWidget(), block: true, side:1}).range(4)
]);
let plugin = ViewPlugin.define(view => {}, {decorations: v => decos});
let state = EditorState.create({
	doc: 'asdf',
	extensions: [basicSetup, plugin]
});
view.setState(state);

Expected behavior:
image

I think the inclusive replace widget is covering the other widget. Does inclusive: false help? Thought that would also have the effect of making the text positions at the ends visible I guess. What kind of feature are you implementing?

I found out that the API allows developers to manually set the startSide and endSide values after the Decoration has been created, using which I’ve set my widget to 1e8 * 5 which helped it show up.

Using inclusive: false does make it work as well, but the side effect is that an empty cm-line is shown as the replacement does not cover the whole line anymore.

What kind of feature are you implementing?

Well… kinda crazy but I’m attempting to build something like Typora and HyperMD using cm6. It’s really close to mostly working but I’m still experiencing a lot of bugs with respect to janky selection (like when Decorations are added/removed) and some trouble with IMEs creating duplicate text.

I’m using a combination of a ViewPlugin for inline replacements (to hide markdown formatting symbols such as **) and inline widgets (for example, to render markdown images, or to add the little icon
for external links). Here’s what it looks like:

test

I’m also using a StateField for block level replacements and widgets that sometimes span more than one line.

test

No, that’s not something the API actually intends to allow. You should consider all properties on the objects in this library to be read-only.

Do those widgets replace full lines? I can’t really read a screencast like that and figure out what’s going on, sorry, too much moving parts.

No, that’s not something the API actually intends to allow. You should consider all properties on the objects in this library to be read-only.

I see. This was the property I referred to, which I didn’t know should have been read-only.

Do those widgets replace full lines? I can’t really read a screencast like that and figure out what’s going on, sorry, too much moving parts.

From the second gif, the last 2 widgets replaces full lines. I’m trying to add another widget that’s persistently at the end of the document (like a footer). It seems to disappear when the previous widget just happens to replace exactly to the end of the document.

Hm, yeah, this is still pretty awkward. It’s hard to set up straightforward rules for how overlapping/adjacent decorations interact that work in all situations. I will look into this some other time, don’t have time for it now.

Thank you for your time!

This patch further refines decoration ordering, and I think it should cover both the use cases described in this thread and not break any reasonable use of decorations. Please let me know how it works for you if you’ve had a chance to test it.

I will give it a go and check back in once I have some field testing, thank you!

One thing I’m observing with the new patch is that whole-line block replacements no longer replaces the line element. It is now showing an empty cm-line for the first line of the block.

Creating a decoration like Decoration.replace({widget: someWidget, block: true}).range(lineStart, lineEnd) does replace the entire line with a widget for me. What are you doing?

I’ve done some additional testing since this and it seems that this is actually caused by a Decoration.line in conjunction with a whole-line block replacement widget.

Ah, indeed. This patch fixes that.