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

1 Like

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

1 Like

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;
}

1 Like

Thank you!!!