Moving of cursor with different size mark decoration and replace decoration issues

Hello again, my last issue got ignored, but I found the solution. Anyways this issue is different, I have created a plugin which creates a mark decoration which increases the size of text, in my case header1 in the markdown format. This works well, but I found the cursor up and down moves depending on the actual offset(the length) from the left side of the document / textarea, I don’t want this functionality rather I want the movement to be done depending on how many chars is it from the the left side of the document / textarea, this would be the case normally if all the text was the same size, but because of my mark decoration it is not.

The other thing is that I have implemented is a replace decoration, which replaces the hashtag at the start a header1, the way I have implemented this is that within the same function I create my mark decoration I also create my replace decoration, and in another function I delete my replace decoration when the cursor or selection is in the same line, allowing for editing the document. The problem here is that when the cursor is moving up and down using the up and down arrow I noticed the cursor always goes 2 chars ahead of it should be at, which I am uncertain why is the case.

How could I solve these problems? And what is causing my cursor to go 2 chars ahead when the cursor is moved up using the up arrows? Is there a better way to implement this whole code?

import {
  EditorView,
  Decoration,
  ViewPlugin,
} from "@codemirror/view";
import { syntaxTree } from "@codemirror/language";

// Add headers & remove hastags
function headers(view) {
  let headersList = [];

  for (let { from, to } of view.visibleRanges) {
    syntaxTree(view.state).iterate({
      from,
      to,
      enter: (type, from, to) => {
        if (type.name == "ATXHeading1") {
          let deco1 = header1.range(from, to);
          let deco2 = headerhastagremove.range(from, from + 2);
          headersList.push(deco1);
          headersList.push(deco2);
        }
      },
    });
  }
  return Decoration.set(headersList, true);
}

// Show hastags when the cursor is in line
function removehastag(view, decorations) {

  if (view.state.selection.ranges == undefined) return decorations;

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

  for (let { from, to } of view.state.selection.ranges) {
    syntaxTree(view.state).iterate({
      from,
      to,
      enter: (type, from, to) => { // goes through each line
        if (type.name == "ATXHeading1") {
          decorations = decorations.update({
            filter: (f, t, value) => {
              if (value.spec.class == "remove-hashtag") {
                return false; // remove hastag
              } else return true; // skip other wise
            },
            filterFrom: from,
            filterTo: to, // each lines froms and tos.
          });
        }
      },
    });
    console.log(view.state.selection.ranges[0])
    return decorations;
  }
}

// Deco
const header1 = Decoration.mark({ class: "cm-header1" });
const headerhastagremove = Decoration.replace({ class: "remove-hashtag" });

// Theme
export const header1StyleTheme = EditorView.baseTheme({
  ".cm-header1": {
    fontSize: "1.75em",
  },
});

// Plugin / Extention
export const header1Plugin = ViewPlugin.fromClass(
  class {
    constructor(view) {
      this.decorations = headers(view);
    }

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

This is pretty much drag and drop code other than a need for creation EditorState with markdown(), adding the extensions with export at the start, and EditorView.

Arrow key motion is implemented with regular key bindings in defaultKeymap, so you can create custom commands that implement this type of vertical motion and bind them to the arrow keys (don’t forget to also create a selection-extending version for when shift is held down).

The other problem I can’t, at a glance, diagnose, but if you can reduce it to some obvious misbehavior in the library, show a minimal test case and I’ll take a look.

For the first issue I would need to look at the documentation, and I will do so, thanks a lot for the input!

As per the second issue and the main one, I have condensed the code into 40 lines of code, which is hardcoded for the document # Hello World!\n, the \n is used for testing purpose. In this code, a simple remove decoration is first created in a function which replaces the # , and then it is removed in another function through filter, if the cursor is in the range of hello world. For testing the issue, change the cursor between the two lines using the arrow keys and you would see the cursor is always two steps ahead of where it should be.

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

// Remove hastags(`# `) decoration
function removehashtag(view) {
  let removehashtagdecoration = Decoration.replace({}).range(0, 2); // hardcoded range from 0 to 2

  return Decoration.set([removehashtagdecoration], true);
}

// Show hastags when the cursor is in the same range
function addhashtag(view, decorations) {
  if (view.state.selection.ranges == undefined) return decorations;

  // Note, this case is only applicable to `# Hello World!`, to condense the code
  if (view.state.selection.ranges[0].from <= 14) { // If the starting point of the cursor is bellow or at 14 range.
    decorations = decorations.update({
      filter: (f, t, v) => false, // remove all decoration
    });
  }
  return decorations;
}

// Plugin / Extention
export const header1Plugin = ViewPlugin.fromClass(
  class {
    constructor(view) {
      this.decorations = removehashtag(view);
    }

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

To use this code, as mentioned before you would need to change the document to # Hello World!, and import the header1Plugin onto the EditorState which then can be mounted to EditorView.

I have a suspicion that there is something to do with my method of removal of the replace decoration, but I can’t really tell how to fix it, I have no clue here.

What is happening there is that, in order to allow vertical motion to go past short lines without losing its horizontal position, a ‘goal’ position is stored with the cursor. That is determined when the cursor makes the first move in a series of vertical moves, from the position where it starts with the styling that exists at that time.

If you switch to a simpler column-based motion command all that should go away (though you’ll also lose the feature of preserving vertical position through a sequence of moves).

This makes a lot of sense.

And just to make sure I understand everything correctly, the horizontal positioning of the cursor is done though the goal variable which is stored in the cursor, this takes the actual length from the left side of the text area depending on the styling at the moment. The problem comes because of the addition and removal of the replace decoration, which is causing the cursor to go 2 steps ahead.

If I am right, to solve this I should make a custom keymap for vertical movement with higher precedence to the default ones which use column-based motion instead of the goal one, and this should solve both my problems, the previous(of header1 size) and this one?

Again, thanks a lot!

Yes, that sounds right.

1 Like

Well I am still in the process of learning how to add a keymap which replaces the default keymap, I made a interesting discovery, apparently if I use basicSetup as a extension, the bug is still there, but different in the manor as it doesn’t keep adding 2 chars every time I move up and down, but rather only once, and does it again if I move the cursor left or right. I can also replicate this issue on the example I send, where adding basicSetup would make such different issue.

Though I am uncertain why this might be the case, I am hoping this shouldn’t be a different issue than what I am dealing with, I am still working at getting a keymap, maybe you can take a look and state why. Though without a fix I cant say how much of this matters.

Thanks a lot, and ignore this depending on how much this may matter you, it still is useful to others who might stumble on this issue.