What is the equivalent of view.dispatch in a statefield?

Hi!

In a Viewplugin I have access to an EditorView object, and I can use:

view.dispatch({ effects: Collapse.of([doFold.range(CollapseStart, CollapseEnd)]) })

But how would I do the same from a Statefield? Would it be:

transaction.state.update({effects: Collapse.of([doFold.range(CollapseStart, CollapseEnd)])})

This doesn’t seem to work :frowning:

Thanks in advance!

Don’t. State field update methods should just be pure functions that compute a new value. They respond to a state update, and shouldn’t cause state updates.

If you need to add effects to a transaction before it is applied, maybe transactionExtender is what you want.

Hi!

Thank you for your answer. Either I did something wrong, or it just didn’t work for me.

I know you have better things to do, but I would really appreciate your help. There are two things I can’t figure out:

  • If a user types “fold” in the first line of a codeblock, then it should automatically fold the codelines, and if the line already contains fold, then it should be displayed folded at the start, but of course the user should be able to uncollapse it.
  • If a user types “file:” and some string after it, then it should automatically update the widgets text. (searchString is a function, which returns the corresponding values). Right now, if I start to type, then the first letter is displayed. The rest is only displayed if I switch documents.

Thanks in advance!

My current code:

export const CodeblockHeader = StateField.define<DecorationSet>({
  create(state): DecorationSet {
    return Decoration.none;
  },
  update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
    const builder = new RangeSetBuilder<Decoration>();
    let WidgetStart;
    let isFolded = false;
    var fileName = "";
    let CollapseStart = null;
    let CollapseEnd = null;
    syntaxTree(transaction.state).iterate({
      enter(node) {
        const lineText = transaction.state.doc.slice(node.from, node.to).toString();
        if (node.type.id === 3 ) {
          CollapseStart = node.from;
          fileName = searchString(lineText, "file:");
          isFolded = searchString(lineText, "fold");
          WidgetStart = node.from;
          if (fileName !== null && fileName !== "" ){
            builder.add(WidgetStart, WidgetStart, Decoration.widget({
              widget: new TextAboveCodeblockWidget("Testing"),
              block: true}));
          }
        }
        if (node.type.id === 6 ) {
          CollapseEnd = node.to;
          if (isFolded && CollapseStart != null && CollapseEnd != null) {
            console.log("Send collapse event??");
          }
          CollapseStart = null;
          CollapseEnd = null;
          isFolded = false;
        }
        fileName = "";
      },
    });
    return builder.finish();
  },
  provide(field: StateField<DecorationSet>): Extension {
    return EditorView.decorations.from(field);
  },
});

const Collapse = StateEffect.define(), UnCollapse = StateEffect.define()

const markField = StateField.define({
  create() { return Decoration.none },
  update(value, tr) {
    value = value.map(tr.changes)
    for (let effect of tr.effects) {
      if (effect.is(Collapse)) value = value.update({add: effect.value, sort: true})
      else if (effect.is(UnCollapse)) value = value.update({filter: effect.value})
    }
    return value
  },
  provide: f => EditorView.decorations.from(f)
})

const doFold = Decoration.replace({block: true})

class TextAboveCodeblockWidget extends WidgetType {
  text: string;
  constructor(text: string, private isCollapsed: boolean) {
    super();
     this.folded = isCollapsed;
     this.text = text;
  }
  
  eq(other: TextAboveCodeblockWidget) { return other.folded == this.folded }
    
  toDOM(view: EditorView): HTMLElement {
    const container = document.createElement("div");
    container.classList.add("codeblock-header-container");    
    
    const span = document.createElement("div");
    span.innerText = this.text;
    span.classList.add("codeblock-header-text");

    container.appendChild(span);
    
    return container;
  }
  
  ignoreEvent() { return false }
  
}

class Test implements PluginValue {
  decorations: DecorationSet;
  
  constructor(view: EditorView) {
    this.decorations = this.buildDecorations(view);
  }

  update(update: ViewUpdate) {
    if (update.docChanged || update.viewportChanged )
      this.decorations = this.buildDecorations(update.view);    
  }
  
  destroy() {}

  buildDecorations(view: EditorView): DecorationSet {
     let widgets = [];
     return Decoration.set(widgets)
  }
}

const pluginSpec: PluginSpec<Test> = {
  decorations: v => v.decorations,

  eventHandlers: {
    mousedown: (e, view) => {
      let target = e.target as HTMLElement;
      let Pos = view.posAtDOM(target);
      let effect = view.state.field(markField,false);
      let isFolded = false
      effect.between(Pos, Pos, () => { isFolded = true})
      if (target.nodeName == "DIV" &&
          target.parentElement!.classList.contains("codeblock-header-container")){
        let CollapseStart = null;
        let CollapseEnd = null;
        let WidgetStart;
        var fileName = "";
          syntaxTree(view.state).iterate({
            enter(node) {
              if (node.type.id === 3 ) {
                if (Pos === node.from)
                  CollapseStart = node.from;
                WidgetStart = node.from;
              }
              if (node.type.id === 6 ) {
                CollapseEnd = node.to;
                if (CollapseStart != null && CollapseEnd != null ){
                  if (isFolded){
                    view.dispatch({ effects: UnCollapse.of((from, to) => to <= CollapseStart || from >= CollapseEnd) })
                  }
                  else {
                    view.dispatch({ effects: Collapse.of([doFold.range(CollapseStart, CollapseEnd)]) })
                  }
                  CollapseStart = null;
                  CollapseEnd = null;
                  fileName = "";
                }
              }
            },
          });
      }
    },
  }
};

export const test = ViewPlugin.fromClass(
  Test,
  pluginSpec
);

Sorry to disturb again, but I am still struggling with the “default folding”. I tried putting transactionExtender in the CodeblockHeader, but this has no effect at all.

let tr = EditorState.transactionExtender.of({effects: Collapse.of([doFold.range(CollapseStart, CollapseEnd)])})
transaction.state.update(tr);

I can’t figure it out, how this should work. The normal folding is working correctly when a user clicks on the header.
The other problem I could solve.

That snippet is wrong in a whole bunch of ways. A transaction extender is a function that runs for every transaction and can opt to add annotations or effects to it (so the thing given to transactionExtender.of should be a function, see the types in the docs). It is registered in an editor as an extension, so definitely not dispatched as if it is a transaction.

I don’t understand what you are trying to do, but also don’t really have time to implement people’s use cases for them, so at this point I can only advise you to read the docs a bit more closely.

I know it’s wrong in a whole bunch of ways. It is definitely not my best work, but I am still new to this. Your answer and this question Highest precedence transaction extender overrides other transaction extenders definitely helped me a lot to understand better how transctionExtender works.

transctionExtender is what I need (I think). I just don’t need it for every transaction. I just want the effect to apply only when the document is loaded/opened (once). It should apply the initial effect, and that’s it. So basically, I just need an if statement. I noticed that when I open a document the cursor position is always located at 0, but obviously this is not ideal.

Is there something I could check, so the effect doesn’t apply continuously?

Thanks!

Then I misunderstood what you were doing, and transactionExtender is not the right approach. I’d say just implement this as both an extension that implements the folding state, and a function that you call once on a newly created editor to scan for the appropriate text and initialize the folding state.