Repository: jupyter/nbdime (language: python)

Issue:
CM6: Restore collapser
This is a follow-up of https://github.com/jupyter/nbdime/pull/673 in which the collapser of unchanged code was removed (as not ported from CM5 to CM6).

Some solution could probably be extrapolated from https://github.com/codemirror/merge


Discussion / hints:
https://github.com/codemirror/merge appears to be using `Decoration.replace` which was also recommended recently in a [related topic](https://discuss.codemirror.net/t/gutter-plugin-for-github-like-code-hiding/7067). Is there a reason for not depending on https://github.com/codemirror/merge directly? The decoration widgets are modular so possibly we could just import them (possibly with tiny upstream adjustments).
> Is there a reason for not depending on https://github.com/codemirror/merge directly?

Not from me :wink: - I have not tried using its collapser directly.

The only trouble with the default codemirror code is usually the data structure that is not compatible with the one defines by nbdime. So to be tested for the collapser I would say.
>  Is there a reason for not depending on https://github.com/codemirror/merge directly?

As @fcollonval said, nbdime defines its own data structure for diff chunks, not compatible with the one in codemirror/merge. Besides, codemirror/merge provides a 2 ways view only, while nbdime also provides 3 and 4 ways views, which require extra padding for alignment, so I don't see how we could reuse the classes defined in codemirror/merge.

Regarding the decorations defined in codemirror/merge, they seem to be private to me (but I might be wrong here, I'm definitely not a TypeScript expert).

Therefore I don't really see how we could benefit from depending on codemirror/merge (but again I might have missed things)
> Regarding the decorations defined in codemirror/merge, they seem to be private to me

You are right, this is (among others) what I meant by "tiny upstream adjustments" ;)

Thank you for explaining the rationale in more detail. I would still consider it worthwhile trying to contribute some of the things upstream and then reusing theme here, but of course this has an overhead and we may prefer to have a custom implementation for time being.

Fix patch:
diff --git a/package-lock.json b/package-lock.json
index 02a028f6..d87f475d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "jest": "^29.6.4",
         "lerna": "^4.0.0",
         "mocha": "^10.2.0",
+        "npm-run-all": "^4.1.5",
         "npm-which": "^3.0.1",
         "rimraf": "^2.6.3"
       }
@@ -7879,6 +7880,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
+      "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+      "dev": true,
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.0",
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "get-intrinsic": "^1.2.1",
+        "is-array-buffer": "^3.0.2",
+        "is-shared-array-buffer": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/arrify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@@ -9704,6 +9726,20 @@
         "node": ">=10"
       }
     },
+    "node_modules/define-data-property": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
+      "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.2.1",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/define-properties": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
@@ -10123,18 +10159,19 @@
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.21.2",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
-      "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+      "version": "1.22.2",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz",
+      "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==",
       "dev": true,
       "dependencies": {
         "array-buffer-byte-length": "^1.0.0",
+        "arraybuffer.prototype.slice": "^1.0.2",
         "available-typed-arrays": "^1.0.5",
         "call-bind": "^1.0.2",
         "es-set-tostringtag": "^2.0.1",
         "es-to-primitive": "^1.2.1",
-        "function.prototype.name": "^1.1.5",
-        "get-intrinsic": "^1.2.0",
+        "function.prototype.name": "^1.1.6",
+        "get-intrinsic": "^1.2.1",
         "get-symbol-description": "^1.0.0",
         "globalthis": "^1.0.3",
         "gopd": "^1.0.1",
@@ -10149,19 +10186,23 @@
         "is-regex": "^1.1.4",
         "is-shared-array-buffer": "^1.0.2",
         "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.10",
+        "is-typed-array": "^1.1.12",
         "is-weakref": "^1.0.2",
         "object-inspect": "^1.12.3",
         "object-keys": "^1.1.1",
         "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.4.3",
+        "regexp.prototype.flags": "^1.5.1",
+        "safe-array-concat": "^1.0.1",
         "safe-regex-test": "^1.0.0",
-        "string.prototype.trim": "^1.2.7",
-        "string.prototype.trimend": "^1.0.6",
-        "string.prototype.trimstart": "^1.0.6",
+        "string.prototype.trim": "^1.2.8",
+        "string.prototype.trimend": "^1.0.7",
+        "string.prototype.trimstart": "^1.0.7",
+        "typed-array-buffer": "^1.0.0",
+        "typed-array-byte-length": "^1.0.0",
+        "typed-array-byte-offset": "^1.0.0",
         "typed-array-length": "^1.0.4",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.9"
+        "which-typed-array": "^1.1.11"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10918,15 +10959,15 @@
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "node_modules/function.prototype.name": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
-      "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+      "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.19.0",
-        "functions-have-names": "^1.2.2"
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1",
+        "functions-have-names": "^1.2.3"
       },
       "engines": {
         "node": ">= 0.4"
@@ -12512,16 +12553,12 @@
       }
     },
     "node_modules/is-typed-array": {
-      "version": "1.1.10",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
-      "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
       "dev": true,
       "dependencies": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
-        "for-each": "^0.3.3",
-        "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
+        "which-typed-array": "^1.1.11"
       },
       "engines": {
         "node": ">= 0.4"
@@ -15525,6 +15562,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/memorystream": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
     "node_modules/meow": {
       "version": "8.1.2",
       "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz",
@@ -16440,6 +16486,12 @@
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
       "dev": true
     },
+    "node_modules/nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
     "node_modules/node-fetch": {
       "version": "2.6.11",
       "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz",
@@ -16957,6 +17009,98 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "node_modules/npm-run-all": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+      "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "chalk": "^2.4.1",
+        "cross-spawn": "^6.0.5",
+        "memorystream": "^0.3.1",
+        "minimatch": "^3.0.4",
+        "pidtree": "^0.3.0",
+        "read-pkg": "^3.0.0",
+        "shell-quote": "^1.6.1",
+        "string.prototype.padend": "^3.0.0"
+      },
+      "bin": {
+        "npm-run-all": "bin/npm-run-all/index.js",
+        "run-p": "bin/run-p/index.js",
+        "run-s": "bin/run-s/index.js"
+      },
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "dev": true,
+      "dependencies": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      },
+      "engines": {
+        "node": ">=4.8"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+      "dev": true,
+      "dependencies": {
+        "shebang-regex": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/npm-run-all/node_modules/which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
+      }
+    },
     "node_modules/npm-run-path": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -17866,6 +18010,18 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pidtree": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+      "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+      "dev": true,
+      "bin": {
+        "pidtree": "bin/pidtree.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
     "node_modules/pify": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
@@ -18888,14 +19044,14 @@
       }
     },
     "node_modules/regexp.prototype.flags": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
-      "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
+      "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.2.0",
-        "functions-have-names": "^1.2.3"
+        "set-function-name": "^2.0.0"
       },
       "engines": {
         "node": ">= 0.4"
@@ -19228,13 +19384,13 @@
       }
     },
     "node_modules/safe-array-concat": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
-      "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
+      "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.0",
+        "get-intrinsic": "^1.2.1",
         "has-symbols": "^1.0.3",
         "isarray": "^2.0.5"
       },
@@ -19513,6 +19669,20 @@
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
+    "node_modules/set-function-name": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
+      "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+      "dev": true,
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -19552,6 +19722,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/side-channel": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -20146,15 +20325,32 @@
         "node": ">=8"
       }
     },
+    "node_modules/string.prototype.padend": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz",
+      "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/string.prototype.trim": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
-      "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
+      "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4"
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
       },
       "engines": {
         "node": ">= 0.4"
@@ -20164,28 +20360,28 @@
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
-      "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
+      "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4"
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/string.prototype.trimstart": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
-      "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
+      "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.20.4"
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.22.1"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -20936,6 +21132,57 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/typed-array-buffer": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
+      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "get-intrinsic": "^1.2.1",
+        "is-typed-array": "^1.1.10"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/typed-array-byte-length": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
+      "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "has-proto": "^1.0.1",
+        "is-typed-array": "^1.1.10"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-byte-offset": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
+      "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "has-proto": "^1.0.1",
+        "is-typed-array": "^1.1.10"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/typed-array-length": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
@@ -21811,17 +22058,16 @@
       }
     },
     "node_modules/which-typed-array": {
-      "version": "1.1.9",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
-      "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
+      "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
       "dev": true,
       "dependencies": {
         "available-typed-arrays": "^1.0.5",
         "call-bind": "^1.0.2",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0",
-        "is-typed-array": "^1.1.10"
+        "has-tostringtag": "^1.0.0"
       },
       "engines": {
         "node": ">= 0.4"
diff --git a/package.json b/package.json
index 20fc6823..4a806a16 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "private": true,
   "scripts": {
     "build": "lerna run build",
-    "build:dev": "tsc --build",
+    "build:dev": "lerna run build:dev",
     "clean": "lerna run clean",
     "prettier": "prettier --list-different --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
     "prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
@@ -13,7 +13,9 @@
     "update:lab": "update-dependency --lerna --minimal --regex ^@jupyterlab/",
     "update:lab:next": "update-dependency --lerna --minimal --regex ^@jupyterlab/ ^next",
     "updated": "lerna updated",
-    "watch": "tsc --build --watch"
+    "watch:webapp": "run-p watch:lib watch:app",
+    "watch:app": "lerna exec --stream --scope \"nbdime-webapp\" npm run watch",
+    "watch:lib": "lerna exec --stream --scope \"nbdime\" --scope \"jupyterlab-rise\" npm run watch"
   },
   "devDependencies": {
     "@jupyterlab/buildutils": "4.0.0",
@@ -24,6 +26,7 @@
     "jest": "^29.6.4",
     "lerna": "^4.0.0",
     "mocha": "^10.2.0",
+    "npm-run-all": "^4.1.5",
     "rimraf": "^2.6.3",
     "npm-which": "^3.0.1"
   },
diff --git a/packages/nbdime/src/common/mergeview.ts b/packages/nbdime/src/common/mergeview.ts
index db2ba639..55bed148 100644
--- a/packages/nbdime/src/common/mergeview.ts
+++ b/packages/nbdime/src/common/mergeview.ts
@@ -123,14 +123,29 @@ const mergeViewDecorationDict: MergeViewDecorationDict = {
   mixedMerge: Private.buildEditorDecorationDict('m', 'mixed'),
 };
 
+/**
+ * Additional theme element
+ */
+const baseTheme = EditorView.baseTheme({
+  '.cm-collapsedLines': {
+    cursor: 'pointer',
+    color: 'var(--jp-ui-font-color1)',
+    backgroundColor: 'var(--jp-layout-color2)',
+    border: 'var(--jp-border-width) solid var(--jp-border-color1)',
+    fontSize: '90%',
+    padding:'0 3px',
+    borderRadius: '4px',
+  },
+});
+
 /**
  * Get common editor extensions
  *
  * @param isMergeView Whether the editor is within a merge view or not.
  * @returns Editor extensions
  */
-function getCommonEditorExtensions(isMergeView = true): Extension {
-  const extensions = [highlightField, paddingWidgetField];
+function getCommonEditorExtensions(isMergeView = true): Extension[] {
+  const extensions = [baseTheme, highlightField, paddingWidgetField];
 
   return isMergeView
     ? [
@@ -409,6 +424,92 @@ const conflictMarkerLineChunkMappingField = StateField.define<
   },
 });
 
+/**
+ * uncollapseEffect effect to synchronize uncollapseEffectd ranges between editors.
+ */
+const uncollapseEffect = StateEffect.define<number>({
+  map: (value, change) => change.mapPos(value),
+});
+
+/**
+ * Effect to set the collapser widgets in an editor
+ */
+const setCollapsersEffect = StateEffect.define<DecorationSet>({
+  map: (value, mapping) => value.map(mapping),
+});
+
+class CollapseWidget extends WidgetType {
+  constructor(
+    readonly lines: number,
+    readonly siblings: { line: number; cm: EditorView }[],
+  ) {
+    super();
+  }
+
+  eq(other: CollapseWidget) {
+    return (
+      this.lines == other.lines &&
+      this.siblings.every(
+        sibling =>
+          other.siblings.findIndex(
+            otherSibling =>
+              otherSibling.line == sibling.line &&
+              otherSibling.cm == sibling.cm,
+          ) >= 0,
+      )
+    );
+  }
+
+  toDOM(view: EditorView) {
+    let outer = document.createElement('div');
+    outer.className = 'cm-collapsedLines';
+    outer.textContent = view.state.phrase('(…$ unchanged lines…)', this.lines);
+    outer.addEventListener('click', e => {
+      const pos = view.posAtDOM(e.target as HTMLElement);
+      this.siblings.forEach(sibling => {
+        if (sibling.cm === view) {
+          view.dispatch({ effects: uncollapseEffect.of(pos) });
+        } else {
+          const from = sibling.cm.state.doc.line(sibling.line).from;
+          sibling.cm.dispatch({ effects: uncollapseEffect.of(from) });
+        }
+      });
+    });
+    return outer;
+  }
+
+  ignoreEvent(e: Event) {
+    return e instanceof MouseEvent;
+  }
+
+  get estimatedHeight() {
+    return 27;
+  }
+}
+
+/**
+ * StateField storing information about the collapsed ranges
+ */
+const CollapsedRangesField = StateField.define<DecorationSet>({
+  create(state) {
+    return Decoration.none;
+  },
+  update(deco, tr) {
+    for (let e of tr.effects) {
+      if (e.is(setCollapsersEffect)) {
+        return e.value;
+      }
+    }
+
+    deco = deco.map(tr.changes);
+    for (let e of tr.effects)
+      if (e.is(uncollapseEffect))
+        deco = deco.update({ filter: from => from != e.value });
+    return deco;
+  },
+  provide: f => EditorView.decorations.from(f),
+});
+
 export interface IMergeViewOptions {
   remote: IStringDiffModel | null;
   local?: IStringDiffModel | null;
@@ -460,7 +561,7 @@ export class DiffView {
   constructor(
     model: IStringDiffModel,
     type: 'left' | 'right' | 'merge',
-    options: IMergeViewEditorConfiguration,
+    options: Omit<IEditorWidgetOptions, 'value'>,
   ) {
     this._model = model;
     this._type = type;
@@ -971,6 +1072,10 @@ export class DiffView {
     return this._baseEditorWidget;
   }
 
+  get chunks(): Chunk[] {
+    return this._chunks;
+  }
+
   get lineChunks(): Chunk[] {
     return this._lineChunks;
   }
@@ -990,6 +1095,27 @@ export class DiffView {
   private _lockButton: HTMLElement;
 }
 
+/**
+ * From a line in base, find the matching line in another editor by chunks.
+ */
+function getMatchingEditLine(baseLine: number, chunks: Chunk[]): number {
+  let offset = 0;
+  // Start values correspond to either the start of the chunk,
+  // or the start of a preceding unmodified part before the chunk.
+  // It is the difference between these two that is interesting.
+  for (let i = 0; i < chunks.length; i++) {
+    let chunk = chunks[i];
+    if (chunk.baseTo > baseLine && chunk.baseFrom <= baseLine) {
+      return 0;
+    }
+    if (chunk.baseFrom > baseLine) {
+      break;
+    }
+    offset = chunk.remoteTo - chunk.baseTo;
+  }
+  return baseLine + offset;
+}
+
 /**
  * From a line in base, find the matching line in another editor by line chunks
  *
@@ -1016,7 +1142,7 @@ function getMatchingEditLineLC(toMatch: Chunk, chunks: Chunk[]): number {
  *   aligned line #2 ..., etc.]
  */
 
-function findAlignedLines(dvs: DiffView[]): number[][] {
+function findAlignedLines(dvs: Readonly<DiffView[]>): number[][] {
   let linesToAlign: number[][] = [];
   let ignored: number[] = [];
 
@@ -1101,6 +1227,12 @@ function findAlignedLines(dvs: DiffView[]): number[][] {
 }
 export interface IMergeViewEditorConfiguration
   extends Omit<IEditorWidgetOptions, 'value'> {
+  /**
+   * When true stretches of unchanged text will be collapsed. When a number is given, this indicates the amount
+   * of lines to leave visible around such stretches (which defaults to 2). Defaults to true.
+   */
+  collapseIdentical?: boolean | number;
+
   /**
    * Provides remote diff of document to be shown on the right of the base.
    * To create a diff view, provide only remote.
@@ -1140,8 +1272,16 @@ export class MergeView extends Panel {
     let left: DiffView | null = (this._left = null);
     let right: DiffView | null = (this._right = null);
     let merge: DiffView | null = (this._merge = null);
+    // Set to -1 to deactivate
+    if (typeof options.collapseIdentical !== 'undefined') {
+      this._collapseIdentical =
+        typeof options.collapseIdentical === 'number'
+          ? Math.max(-1, options.collapseIdentical)
+          : options.collapseIdentical
+          ? 2
+          : -1;
+    }
     let panes: number = 0;
-    this._diffViews = [];
     let main = options.remote || options.merged;
     if (!main) {
       throw new Error('Either remote or merged model needs to be specified!');
@@ -1235,9 +1375,12 @@ export class MergeView extends Panel {
      *     - Partial changes: Use base + right editor
      */
     const inMergeView = !!merged;
-    const additionalExtensions: Extension = inMergeView
+    const additionalExtensions = inMergeView
       ? [listener, mergeControlGutter, getCommonEditorExtensions(inMergeView)]
       : getCommonEditorExtensions(inMergeView);
+    if(this._collapseIdentical >= 0) {
+      additionalExtensions.push(CollapsedRangesField)
+    }
 
     this._base = new EditorWidget({
       ...options,
@@ -1270,7 +1413,6 @@ export class MergeView extends Panel {
           config: { ...options.config },
           extensions: [options.extensions ?? [], additionalExtensions],
         });
-        this._diffViews.push(left);
         leftWidget = left.remoteEditorWidget;
       }
       this._gridPanel.addWidget(leftWidget);
@@ -1295,7 +1437,6 @@ export class MergeView extends Panel {
           config: { ...options.config },
           extensions: [options.extensions ?? [], additionalExtensions],
         });
-        this._diffViews.push(right);
         rightWidget = right.remoteEditorWidget;
       }
       this._gridPanel.addWidget(rightWidget);
@@ -1308,7 +1449,6 @@ export class MergeView extends Panel {
         config: { ...options.config, readOnly },
         extensions: [options.extensions ?? [], additionalExtensions],
       });
-      this._diffViews.push(merge);
       let mergeWidget = merge.remoteEditorWidget;
       this._gridPanel.addWidget(mergeWidget);
       mergeWidget.addClass('cm-merge-editor');
@@ -1336,7 +1476,6 @@ export class MergeView extends Panel {
           config: { ...options.config },
           extensions: [options.extensions ?? [], additionalExtensions],
         });
-        this._diffViews.push(right);
         let rightWidget = right.remoteEditorWidget;
         rightWidget.addClass('cm-diff-right-editor');
         this.addWidget(new Widget({ node: right.buildGap() }));
@@ -1351,6 +1490,11 @@ export class MergeView extends Panel {
         dv.init(this._base);
       }
     }
+
+    if (this._collapseIdentical >= 0) {
+      this.collapseIdenticalStretches();
+    }
+
     this._aligning = false;
     if (this._diffViews.length > 0) {
       this.scheduleAlignViews();
@@ -1471,6 +1615,92 @@ export class MergeView extends Panel {
     return this.merge.remoteEditorWidget.doc.toString();
   }
 
+  protected collapseIdenticalStretches(): void {
+    const margin = this._collapseIdentical;
+    if (margin < 0) {
+      return;
+    }
+
+    // Build an array of line that are not part of a chunks
+    const baseEditor = this.base.cm;
+    const clear = new Array<boolean>(baseEditor.state.doc.lines).fill(true);
+    // Collapsers per editor
+    const builders = [new RangeSetBuilder<Decoration>()];
+
+    if (this.left) {
+      builders.push(new RangeSetBuilder<Decoration>());
+      unclearNearChunks(this.left, margin, clear);
+    }
+    if (this.right) {
+      builders.push(new RangeSetBuilder<Decoration>());
+      unclearNearChunks(this.right, margin, clear);
+    }
+    if (this.merge) {
+      builders.push(new RangeSetBuilder<Decoration>());
+      unclearNearChunks(this.merge, margin, clear);
+    }
+
+    for (let i = 0; i < clear.length; i++) {
+      if (clear[i]) {
+        // Lines are 1-based
+        const line = i + 1;
+        let size = 1;
+        for (; i < clear.length - 1 && clear[i + 1]; i++, size++) {
+          // Just finding size of identical stretch
+        }
+        if (size > margin) {
+          // Store the corresponding collapser positions
+          const editors: { line: number; cm: EditorView }[] = [
+            { line: line, cm: baseEditor },
+          ]; // Collapser in the reference editor
+          if (this.left) {
+            editors.push({
+              line: getMatchingEditLine(line, this.left.chunks),
+              cm: this.left.remoteEditorWidget.cm,
+            });
+          }
+          if (this.right) {
+            editors.push({
+              line: getMatchingEditLine(line, this.right.chunks),
+              cm: this.right.remoteEditorWidget.cm,
+            });
+          }
+          if (this.merge) {
+            editors.push({
+              line: getMatchingEditLine(line, this.merge.chunks),
+              cm: this.merge.remoteEditorWidget.cm,
+            });
+          }
+
+          // Create collapser for this strech
+          editors.forEach((editor, idx) => {
+            const from = editor.cm.state.doc.line(editor.line).from;
+            const to = editor.cm.state.doc.line(editor.line + size - 1).to;
+            builders[idx].add(
+              from,
+              to,
+              Decoration.replace({
+                widget: new CollapseWidget(size, editors),
+                block: true,
+              }),
+            );
+          });
+        }
+      }
+    }
+
+    const editors = [
+      baseEditor,
+      ...this._diffViews.map(dv => dv.remoteEditorWidget.cm),
+    ];
+    builders.forEach((builder, idx) => {
+      const decorationSet = builder.finish();
+      editors[idx].dispatch({
+        effects: setCollapsersEffect.of(decorationSet),
+      });
+    });
+  }
+
   /**
    * Actions and updates performed when a gutter marker is clicked
    */
@@ -1570,28 +1800,34 @@ export class MergeView extends Panel {
     this._base.cm.dispatch({ effects });
   }
 
-  public get left(): DiffView | null {
+  get left(): DiffView | null {
     return this._left;
   }
 
-  public get right(): DiffView | null {
+  get right(): DiffView | null {
     return this._right;
   }
 
-  public get merge(): DiffView | null {
+  get merge(): DiffView | null {
     return this._merge;
   }
 
-  public get base(): EditorWidget {
+  get base(): EditorWidget {
     return this._base;
   }
 
+  private get _diffViews(): Readonly<DiffView[]> {
+    return [this.left, this.right, this.merge].filter(
+      dv => dv !== null,
+    ) as DiffView[];
+  }
+
+  private _collapseIdentical: number = 2;
   private _gridPanel: Panel;
   private _left: DiffView | null;
   private _right: DiffView | null;
   private _merge: DiffView | null;
   private _base: EditorWidget;
-  private _diffViews: DiffView[];
   private _aligning: boolean;
   private _measuring: number;
 }
@@ -1621,3 +1857,19 @@ function elt(
   }
   return e;
 }
+
+function unclearNearChunks(
+  dv: DiffView,
+  margin: number,
+  clear: boolean[],
+): void {
+  for (let i = 0; i < dv.chunks.length; i++) {
+    const chunk = dv.chunks[i];
+    for (let l = chunk.baseFrom - margin; l < chunk.baseTo + margin; l++) {
+      const pos = l;
+      if (pos >= 0 && pos < clear.length) {
+        clear[pos] = false;
+      }
+    }
+  }
+}
diff --git a/packages/nbdime/src/styles/diff.css b/packages/nbdime/src/styles/diff.css
index 57f6a316..f512dc4f 100644
--- a/packages/nbdime/src/styles/diff.css
+++ b/packages/nbdime/src/styles/diff.css
@@ -193,8 +193,7 @@
 
 /* border-top/bottom without taking up space */
 .jp-Notebook-diff .cm-diff-right-editor .cm-merge-r-chunk-start::before,
-.jp-Notebook-diff .cm-diff-right-editor .cm-merge-r-chunk-end::after
-{
+.jp-Notebook-diff .cm-diff-right-editor .cm-merge-r-chunk-end::after {
   content: '';
   width: 100%;
   height: 1px;
diff --git a/packages/webapp/package.json b/packages/webapp/package.json
index e301bcb3..fd1ae8cc 100644
--- a/packages/webapp/package.json
+++ b/packages/webapp/package.json
@@ -8,7 +8,7 @@
     "build:dev": "webpack --mode=development",
     "clean": "rimraf build && rimraf -g \"../../nbdime/webapp/static/!(favicon.ico)\"",
     "profile": "webpack --profile --json > webpack-stats.json",
-    "watch": "webpack --watch"
+    "watch": "webpack --mode=development --watch"
   },
   "dependencies": {
     "@fortawesome/fontawesome-free": "^5.12.0",


Test patch:
diff --git a/examples/example10/center.ipynb b/ui-tests/data/merge_test4/center.ipynb
similarity index 92%
rename from examples/example10/center.ipynb
rename to ui-tests/data/merge_test4/center.ipynb
index 9bc3d89d..21131531 100644
--- a/examples/example10/center.ipynb
+++ b/ui-tests/data/merge_test4/center.ipynb
@@ -11,13 +11,11 @@
     "import matplotlib.pyplot as plt\n",
     "\n",
     "def gaussian(x, a, b, c):\n",
-    "    #calculate a gaussian\n",
     "    return a * np.exp(-b * (x-c)**2)\n",
     "\n",
     "def sinus ():\n",
     "    # Here you can see a sinus function\n",
-    "    nx = 100\n",
-    "    x = np.linspace(-5.0, 5.0, nx)\n",
+    "    x = np.linspace(-5.0, 5.0, 100)\n",
     "    y = np.sin(x)\n",
     "    return x, y\n",
     "\n",
@@ -28,6 +26,8 @@
     "    y = gaussian(x, a=2.0, b=0.5, c=1.5)\n",
     "    noise = np.random.normal(0.0, 0.2, nx)\n",
     "    y += noise\n",
+    "    # add a line\n",
+    "    # add a second line\n",
     "    return x, y"
    ]
   }
diff --git a/examples/example10/left.ipynb b/ui-tests/data/merge_test4/left.ipynb
similarity index 80%
rename from examples/example10/left.ipynb
rename to ui-tests/data/merge_test4/left.ipynb
index 3db17ca2..2384cece 100644
--- a/examples/example10/left.ipynb
+++ b/ui-tests/data/merge_test4/left.ipynb
@@ -9,22 +9,25 @@
    "source": [
     "import numpy as np\n",
     "import matplotlib.pyplot as plt\n",
-    "%matplotlib inline\n",
-    "%this is a comment\n",
-    "% more more comments\n",
     "\n",
     "def gaussian(x, a, b, c):\n",
     "    return a * np.exp(-b * (x-c)**2)\n",
-    "    % here a comment\n",
-    "    % second line of comment\n",
+    "\n",
+    "def sinus ():\n",
+    "    # Here you can see a sinus function\n",
+    "    x = np.linspace(-5.0, 5.0, 100)\n",
+    "    y = np.sin(x)\n",
+    "    return x, y\n",
     "\n",
     "def noisy_gaussian():\n",
     "    # gaussian array y in interval -5 <= x <= 5\n",
-    "    nx = 100\n",
+    "    nx = 200\n",
     "    x = np.linspace(-5.0, 5.0, nx)\n",
     "    y = gaussian(x, a=2.0, b=0.5, c=1.5)\n",
     "    noise = np.random.normal(0.0, 0.2, nx)\n",
     "    y += noise\n",
+    "    # add a line\n",
+    "    # add a second line\n",
     "    return x, y"
    ]
   }
diff --git a/examples/example10/right.ipynb b/ui-tests/data/merge_test4/right.ipynb
similarity index 86%
rename from examples/example10/right.ipynb
rename to ui-tests/data/merge_test4/right.ipynb
index 7fe26d4e..8592ae15 100644
--- a/examples/example10/right.ipynb
+++ b/ui-tests/data/merge_test4/right.ipynb
@@ -13,6 +13,12 @@
     "def gaussian(x, a, b, c):\n",
     "    return a * np.exp(-b * (x-c)**2)\n",
     "\n",
+    "def sinus ():\n",
+    "    # Here you can see a sinus function\n",
+    "    x = np.linspace(-5.0, 5.0, 100)\n",
+    "    y = np.sin(x)\n",
+    "    return x, y\n",
+    "\n",
     "def noisy_gaussian():\n",
     "    # gaussian array y in interval -5 <= x <= 5\n",
     "    nx = 100\n",
diff --git a/ui-tests/package-lock.json b/ui-tests/package-lock.json
index 827e33f3..9d389c9b 100644
--- a/ui-tests/package-lock.json
+++ b/ui-tests/package-lock.json
@@ -40,7 +40,6 @@
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
       "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
       "hasInstallScript": true,
       "optional": true,
       "os": [
@@ -50,6 +49,23 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
+      "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
+      "dependencies": {
+        "playwright-core": "1.38.1"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
     "node_modules/playwright-core": {
       "version": "1.36.2",
       "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz",
@@ -61,6 +77,17 @@
       "engines": {
         "node": ">=16"
       }
+    },
+    "node_modules/playwright/node_modules/playwright-core": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
+      "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
     }
   }
 }
diff --git a/ui-tests/playwright.config.js b/ui-tests/playwright.config.js
index 21c63724..c92fabe4 100644
--- a/ui-tests/playwright.config.js
+++ b/ui-tests/playwright.config.js
@@ -8,6 +8,7 @@ module.exports = {
     ['html', { open: process.env.CI ? 'never' : 'on-failure' }],
   ],
   reportSlowTests: null,
+  retries: process.env.CI ? 1 : 0,
   timeout: 60000,
   use: {
     // Browser options
diff --git a/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-linux.png b/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-linux.png
index fe406d11..e2c63d95 100644
Binary files a/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-linux.png and b/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-win32.png b/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-win32.png
index 5e87a561..4443935a 100644
Binary files a/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-win32.png and b/ui-tests/tests/nbdime-diff-test3.spec.ts-snapshots/diff-test3-take-a-snapshot-at-opening-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-linux.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-linux.png
index 13c1dfa3..fed56bab 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-linux.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-win32.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-win32.png
index 983e104b..6c5661d9 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-win32.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-central-version-for-conflict-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-linux.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-linux.png
index 520d7c54..ea782c3d 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-linux.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-win32.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-win32.png
index dd40b435..2b4046ed 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-win32.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-left-version-for-conflict-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-linux.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-linux.png
index 43e2c7b7..ca830596 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-linux.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-win32.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-win32.png
index a5460e4d..ee41b8f3 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-win32.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-choose-right-version-for-conflict-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-take-a-snapshot-at-opening-1-linux.png b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-take-a-snapshot-at-opening-1-linux.png
index 5f89dc0d..d90db101 100644
Binary files a/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-take-a-snapshot-at-opening-1-linux.png and b/ui-tests/tests/nbdime-merge-test1.spec.ts-snapshots/merge-test1-take-a-snapshot-at-opening-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-linux.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-linux.png
index 74cbb12e..725a41a7 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-linux.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-win32.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-win32.png
index c2804767..9c9fa6f6 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-win32.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-central-version-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-linux.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-linux.png
index efe21835..6c1fa427 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-linux.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-win32.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-win32.png
index 84a362a8..d9c696f9 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-win32.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-left-version-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-linux.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-linux.png
index efe21835..6c1fa427 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-linux.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-win32.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-win32.png
index 84a362a8..d9c696f9 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-win32.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-choose-right-version-1-win32.png differ
diff --git a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-take-a-snapshot-at-opening-1-linux.png b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-take-a-snapshot-at-opening-1-linux.png
index efe21835..6c1fa427 100644
Binary files a/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-take-a-snapshot-at-opening-1-linux.png and b/ui-tests/tests/nbdime-merge-test2.spec.ts-snapshots/merge-test2-take-a-snapshot-at-opening-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test4.spec.ts b/ui-tests/tests/nbdime-merge-test4.spec.ts
new file mode 100644
index 00000000..6a90a2eb
--- /dev/null
+++ b/ui-tests/tests/nbdime-merge-test4.spec.ts
@@ -0,0 +1,37 @@
+import { expect, test } from '@playwright/test';
+
+test.beforeEach(async ({ page }) => {
+  await page.goto('http://localhost:41000/merge');
+  await page.locator('#merge-local').fill('data/merge_test4/left.ipynb');
+  await page.locator('#merge-base').fill('data/merge_test4/center.ipynb');
+  await page.locator('#merge-remote').fill('data/merge_test4/right.ipynb');
+  await page.getByRole('button', { name: 'Merge files' }).click();
+});
+
+/* notebooks of same length and 1 conflict*/
+test.describe('merge test4', () => {
+  test('should synchronize the collapse status between editor', async ({ page }) => {
+    expect.soft(await page.locator('#main').screenshot()).toMatchSnapshot();
+
+    // Should display 8 collapsers
+    const collapsers1 = page.getByText('12 unchanged lines');
+    await expect.soft(collapsers1).toHaveCount(4);
+    const collapsers2 = page.getByText('5 unchanged lines');
+    await expect.soft(collapsers2).toHaveCount(4);
+    await expect.soft(page.getByText('import numpy')).toHaveCount(0);
+    await expect.soft(page.getByText('noise = np.random.normal(0.0, 0.2, nx)')).toHaveCount(0);
+
+
+    // Click on the base editor collapsers
+    await page.getByText('12 unchanged lines').nth(1).click();
+    await expect.soft(collapsers1).toHaveCount(0);
+    await page.getByText('5 unchanged lines').nth(1).click();
+    await expect.soft(collapsers2).toHaveCount(0);
+
+    // Should not display any collapser
+
+    await expect(page.getByText('import numpy')).toHaveCount(4);
+    await expect(page.getByText('noise = np.random.normal(0.0, 0.2, nx)')).toHaveCount(4);
+
+  });
+});
diff --git a/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-linux.png b/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-linux.png
new file mode 100644
index 00000000..1c6034d4
Binary files /dev/null and b/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-linux.png differ
diff --git a/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-win32.png b/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-win32.png
new file mode 100644
index 00000000..31ce1fac
Binary files /dev/null and b/ui-tests/tests/nbdime-merge-test4.spec.ts-snapshots/merge-test4-should-synchronize-the-collapse-status-between-editor-1-win32.png differ