Weird behaviour(possibly bug) with replace and mark decoration when double space is used for '. '

Hello again, quite similar to my last issue, but this one a bit different, I also found a solution for this one.

Apparently creating mark decoration on replace decoration in my case ** causes this problem. If I where not create any mark decoration on **, it works as intended. The problem is that instead of getting Hello World!. when I add double spacing after World! I get Hello World . with the character in fount(left side) getting replaced, I recommend testing the issue yourself to understand it. If it matters I used macOS.

import { Decoration, ViewPlugin, EditorView, DecorationSet, ViewUpdate } from "@codemirror/view";

// Note, this case is only applicable to `**Hello World!**\n\n`, to condense the code

// Remove asterick(`**`) decoration
function removeasterisk(view: EditorView) {
  let remove1 = Decoration.replace({}).range(0, 2); // '**Hell..' replace '**'
  // let mark_decoration = Decoration.mark({}).range(2,14) // 'Hello World!', this works as intended
  let mark_decoration = Decoration.mark({}).range(0,16) // 'Hello World!', this doesn't works as intended
  let remove2 = Decoration.replace({}).range(14, 16); // '..orld!**' replace '**'

  return Decoration.set([remove1, mark_decoration, remove2], true);
}


// Show asterick when the cursor is in the same range
function addasterisk(view:EditorView, decorations:DecorationSet) {
  if (view.state.selection.ranges == undefined) return decorations;

  console.log(view.state.selection.ranges[0]);
  

  if (view.state.selection.ranges[0].from <= 16) { // If the starting point of the cursor is bellow or at 16 range.
    decorations = decorations.update({
      filter: (f, t, v) => false, // remove all decoration
    });
  }
  return decorations;
}

// Plugin / Extention
export const Plugin = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;
    
    constructor(view:EditorView) {
      this.decorations = removeasterisk(view);
    }

    update(update:ViewUpdate) {
      if (update.docChanged || update.viewportChanged || update.selectionSet) {
        this.decorations = removeasterisk(update.view);
        this.decorations = addasterisk(update.view, this.decorations);
      }
    }
  },
  {
    decorations: (v) => v.decorations,
  }
);

Though I found a solution I feel like this is a possible bug if not, can you explain why this might be occurring.

Thanks a lot!

Any comment or update on this issue @marijn? I also found that my hot fix doesn’t seem to work with italics or single *, as it replaces one in the end when the bug is reproduced.

Loading that code, setting the document to "**Hello World!**., and then moving the cursor in and out of the range, I don’t see anything out of the ordinary. Maybe you can explain your problem more concretely?

Sorry I may not have been clear, you’d need to set document to **Hello World!**, not **Hello World!**., then hit space bar two times in quick succession, this should result in **Hello World**. (note the space at the end), with the ** hidden because of the hidden decoration, but actually results in **Hello World** . with the character in fount(left side) getting replaced.

Heres a demo with basicSetup:
Screen-Recording-2022-04-24-at-12.23.48-AM

Something similar happens with italics, here first the * is removed with 3 spaces, and then no . is created as at all if repleted, all that that is there is two spaces, that would be created if there was no binding for . :
Screen-Recording-2022-04-24-at-12.37.53-AM


If it matters, my hot-fix was to only mark decorate(italics/bold) the ranges without the *(s). This worked for bold, but didn’t for italic. I can also reproduce this on Obsidian, which uses CM 6.

In here for the example for *Hello World!*\n\n, I marked the decoration within the *(s), without the *(s), added a class, which is styled, and not removed when filtering the decorations to remove the replace decoration, its not commented well, but the code doesn’t change much from my prior codebase.

import { Decoration, ViewPlugin, EditorView, DecorationSet, ViewUpdate } from "@codemirror/view";

// Note, this case is only applicable to `*Hello World!*\n\n`, to condense the code

// Remove asterick(`**`) decoration
function removeasterisk(view: EditorView) {
  let remove1 = Decoration.replace({}).range(0, 1); // '**Hell..' replace '**'
  // let mark_decoration = Decoration.mark({}).range(2,14) // 'Hello World!', this works as intended
  let mark_decoration = Decoration.mark({ class: "cm-italics" }).range(1,13) // 'Hello World!', this doesn't works as intended
  let remove2 = Decoration.replace({}).range(13, 14); // '..orld!**' replace '**'

  return Decoration.set([remove1, mark_decoration, remove2], true);
}

export const StyleTheme = EditorView.baseTheme({
  ".cm-italics": {
    "font-style": "italic"
  },
});

// Show asterick when the cursor is in the same range
function addasterisk(view:EditorView, decorations:DecorationSet) {
  if (view.state.selection.ranges == undefined) return decorations;

  console.log(view.state.selection.ranges[0]);
  

  if (view.state.selection.ranges[0].from <= 14) { // If the starting point of the cursor is bellow or at 16 range.
    decorations = decorations.update({
      filter: (f, t, v) => {
        if (v.spec.class != "cm-italics") return false // if bold don't remove decoration, doesn't really matter to the problem.
        else return true
      },
    });
  }
  return decorations;
}

// Plugin / Extention
export const Plugin = ViewPlugin.fromClass(
  class {
    decorations: DecorationSet;
    
    constructor(view:EditorView) {
      this.decorations = removeasterisk(view);
    }

    update(update:ViewUpdate) {
      if (update.docChanged || update.viewportChanged || update.selectionSet) {
        this.decorations = removeasterisk(update.view);
        this.decorations = addasterisk(update.view, this.decorations);
      }
    }
  },
  {
    decorations: (v) => v.decorations,
  }
);

Heres the demo for the prior code(italics, with mark decoration only within the *):
Screen-Recording-2022-04-24-at-1.18.57-AM_2

Heres the demo for bold version of the prior code(bold, with mark decoration only within the **), this matters because in this case we are getting wanted results:
Screen-Recording-2022-04-24-at-1.11.35-AM

Thanks.