Synchronous Fold/Unfold in MergeView

Hi all,

I’m using MergeView plugin to show the differance between 2 json strings. Super simple code is…

import { json } from ‘@codemirror/lang-json’;
import { MergeView } from ‘@codemirror/merge’;
import { basicSetup } from ‘codemirror’;

new MergeView({
a: {
doc: json1,//change to your json
extensions: [basicSetup, json()]
},
b: {
doc: json2,//change to your json
extensions: [basicSetup, json()
},
parent: element//container Element
});

The problem is when one side is folded/unfolded based on gutter click, other side remains as it was.
How can I make 2 sides to fold/unfold together?

I see click handlers in the native code like
view.dispatch({ effects: unfoldEffect.of(folded) });
or
view.dispatch({ effects: foldEffect.of(range) });

Can I somehow listen to the afterFold/afterUnfold events or something of that king for one editor, and trigger the same action on the other editor?

I’m new in CodeMirror, would appreciate your help.

Thank you

You could observe transactions with fold effects, find the matching line on the other side (by using the chunk information), and fold that.

I believe there would be some shorter solution, but if not, here is working one if someone would need.

import { json } from ‘@codemirror/lang-json’;
import { MergeView } from ‘@codemirror/merge’;
import { EditorView, basicSetup } from ‘codemirror’;
import { foldEffect, foldNodeProp, foldService, foldState, syntaxHighlighting, syntaxTree, unfoldEffect } from ‘@codemirror/language’;
import { EditorState, Extension, SelectionRange, StateEffect, StateEffectType, StateField, Transaction } from ‘@codemirror/state’;//Remove what is not necessary

transactionHandler =
EditorState.transactionFilter.of(tr => {
//this part is responsible to listen to fold/unfold events on one of the editors.
for (const effect of tr.effects) {
if (effect.is(foldEffect) || effect.is(unfoldEffect)) { //Check if transaction is one we need
const otherEditorView = editor.a.state === tr.startState ? editor.b : editor.a;
setTimeout(() => {//without timeout there are some errors happening. Seems we need to wait
//initial transaction to finish
this.foldUnfoldPairEditor(tr, effect, otherEditorView);
}, 0);
}
}
return [tr];
});

editor: MergeView = new MergeView({
a: {
doc: json1,//change to your json
extensions: [basicSetup, json(), this.transactionHandler]
},
b: {
doc: json2,//change to your json
extensions: [basicSetup, json(), this.transactionHandler]
},
parent: element//container Element
});

foldUnfoldPairEditor(tr: Transaction, effect: StateEffect, otherEditorView: EditorView): void {
if (this.foldInProgress) {
//We need this to avoid recursive calles. Skipping second call from pair editor.
this.foldInProgress = false;
} else {
this.foldInProgress = true;
if (effect.is(foldEffect) || effect.is(unfoldEffect)) {
//finally you need position, but working with lines is easier, so example is with lines.
const currentLine = tr.startState.doc.lineAt(effect.value.from);
const otherEditorLine = this.getMatchingLine(currentLine.number);//Your match funciton
if (otherEditorLine != -1) {
const otherEditorBlock = otherEditorView.lineBlockAt(otherEditorView.state.doc.line(otherEditorLine).from);
if (effect.is(foldEffect)) {
//For some reason foldable funciton is not exported from language module, so duplicating it here, and adding necessary dependancies.
let range = this.foldable(otherEditorView.state, otherEditorBlock.from, otherEditorBlock.to);
otherEditorView.dispatch({ effects: foldEffect.of(range) });//Action for pair editor
} else {
// Same for findFold funciton.
let folded = this.findFold(otherEditorView.state, otherEditorBlock.from, otherEditorBlock.to);
otherEditorView.dispatch({ effects: unfoldEffect.of(folded) });//Action for pair editor
}
} else {
this.foldInProgress = false;
}
}
}
}
//Duplicate from language module
findFold(state, from, to) {
var _a;
let found = null;
(_a = state.field(foldState, false)) === null || _a === void 0 ? void 0 : _a.between(from, to, (from, to) => {
if (!found || found.from > from)
found = { from, to };
});
return found;
}

//Duplicate from language module
foldable(state, lineStart, lineEnd) {
for (let service of state.facet(foldService)) {
let result = service(state, lineStart, lineEnd);
if (result)
return result;
}
return this.syntaxFolding(state, lineStart, lineEnd);
}

//Duplicate from language module
syntaxFolding(state, start, end) {
let tree = syntaxTree(state);
if (tree.length < end)
return null;
let stack = tree.resolveStack(end, 1);
let found = null;
for (let iter = stack; iter; iter = iter.next) {
let cur = iter.node;
if (cur.to <= end || cur.from > end)
continue;
if (found && cur.from < start)
break;
let prop = cur.type.prop(foldNodeProp);
if (prop && (cur.to < tree.length - 50 || tree.length == state.doc.length || !this.isUnfinished(cur))) {
let value = prop(cur, state);
if (value && value.from <= end && value.from >= start && value.to > end)
found = value;
}
}
return found;
}

//Duplicate from language module
isUnfinished(node) {
let ch = node.lastChild;
return ch && ch.to == node.to && ch.type.isError;
}

getMatchingLine(line: number): number {
return line;
}

Thank you!!!