Easily track & remove content with decorations

Say I have a setup like the Chrome DevTools Console, where individual entries are added and can be removed via limits (only render last 50 logs) or something like console.clear().

What’s the proper way to add an entry and track its position so it can be removed?

Adding an entry should:

  • Insert a value at the end of the document
  • Optionally apply decorations (a mark and/or line decorations covering multiple lines)

Removing an entry should:

  • Remove that value
  • Remove any decorations
  • Update ranges of other entries??

Previously in CodeMirror 5, I used Bookmarks to help track the positions for later removal, and the line classes / line widgets were easy to manage.

The basics of CodeMirror 6 are really tough to get into. It’s really hard to figure out the right types and methods to use in the API.

I’ve referenced docs on RangeSets/StateFields, the examples for decorations, and various topics in the forum, but there are so many new concepts in CM6 that I’m at a loss for the “proper” way to insert and track all this data.

Here’s a variant of the approach I’ve been trying based on what I’ve found.

const addConsoleEntry = StateEffect.define();
// Should I use a `StateField` or a `RangeSet` for these entries?
const consoleEntriesField = StateField.define({
  // Do I need a `create` or `provide` functions? What value should they return?
  update(consoleEntries, tr) {
    consoleEntries = consoleEntries.map(tr.changes);
    for (let e of tr.effects) {
      if (e.is(addConsoleEntry)) {
        let log = e.value;
        let from = tr.state.doc.length;
        // Can I inject a value into the doc from here, or is that not allowed in `StateEffects` since this transaction may already have `changes`?
        // Once I've injected the value, do I add a `Range` to `consoleEntries` to track this value?
        consoleEntries = consoleEntries.update({
          add: [Range({ from, to, value }) ],
        });

          // Loop through lines and add decorations
          for (let pos = from; pos <= end; ) {
            let line = tr.state.doc.lineAt(pos);
            consoleEntries = consoleEntries.update({
              add: [ consoleLineDecoration.range(line.from) ],
            });
            // Next line
            pos = line.to + 1;
          }
      }
    }
    return consoleEntries;
  },
});

function addConsoleLog(view, log) {
  let effects = [addConsoleEntry.of(log)];

  // Ensure that the necessary extensions are added.
  if (!view.state.field(consoleEntriesField, false)) {
    effects.push(
      StateEffect.appendConfig.of([consoleEntriesField, consoleEntriesTheme])
    );
  }

  view.dispatch({
    // Do I have to make the `changes` of inserting the value here, and not in the effect?
    effects,
  });

  // How can I reference this exact entry to remove it?
}

The way to do something like bookmarks would be to create a state field that tracks the positions you are interested in, and on any transaction where !tr.changes.empty, map all those positions to their new equivalent with tr.changes.mapPos. Using a range set might be an optimization for this (it is more clever about mapping large numbers of positions without looking at every single one of them).

Thanks for the response.

Storing data as a RangeSet makes (theoretical) sense. However, even after spending a week steeped in the CodeMirror 6 docs, I constantly find myself struggling with the practical implementation.

For example: You recommend using RangeSet.

  1. There aren’t any direct examples of RangeSet I can find, so…
  2. I go to the RangeSet Docs
  3. Dig through dozen or more methods to even see the ways to create a RangeSet.
  4. It’s still unclear if I should use new RangeSet(), RangeSet.of(), RangeSet.empty, or if I need to use the completely separate RangeSetBuilder.

After all of that, I still have to figure out:

  1. How to integrate that into StateField
  2. How to properly store data in the RangeSet and StateField
  3. How to loop through the RangeSet to update the position with tr.changes.mapPos
  4. How to get that data back and use it to remove a range of the document.

I’m not complaining about having to learn new concepts, but there are so many disparate concepts/types and so few examples that really showcase how these concepts are supposed to work individually or together.

Some of that will come with time and community, but if there are parts of the documentation or codebase I might be missing that help with understanding how to implement or use them, do let me know.

Noting other discussions for reference:

2 Likes

Since the constructor isn’t in the docs (and thus private), use of or empty depending on whether you want an empty set. As its documentation states, RangeSetBuilder is an optimization for building sets quickly.

I know there’s a lot going on in the library, but I don’t really think you can expect writing custom extensions to be some kind of no-brainer. You’re going to have to learn and combine concepts, and that’s okay.

Put it in the state field. That’s what state fields do, they hold data.

Attach information about what you’re doing to a transaction (probably as a StateEffect) and update your rangeset with its update method when it sees a transaction like that.

See RangeSet.map.

RangeSet.between.

1 Like

Of course. I’m trying to invest the time to understand it. Just a bit tough to pull all these concepts together.

Thanks for the additional clarifications.

1 Like