I would like to get a conversation started about a potential new CodeMirror extension that we have been pondering for some time. Initially I would like to describe our idea and thoughts about the implementation, and get some feedback from people more familiar with CodeMirror’s extensibility points and APIs than we are (in particular from you @Marijn of course, but also from anyone who has insights) to gather an overall assessment on whether this would be feasible to do, and a sense of how difficult or easy it would be.
Depending on what people think, there are of course several options for next steps to try to get this implemented. We can discuss this at a later stage.
Before I go any further, I want to clarify: this is not about building a three-way merge editor as many others have suggested already. This proposal is something much simpler (presumably) but related - more information on that follows below.
Background
Conflict markers
When using git merge
there are sometimes conflicts that Git cannot automatically resolve. Some conflicts involve files that have been modified on both sides of the merge (theirs and ours) and where both sides modified the same lines in the file, such that Git cannot automatically resolve. I contiguous range of such lines we can call a conflict section. These sections are left as conflict markers in the merged files, for the user to resolve as she sees fit.
Here is a simple example:
Michael James Anderson
Sarah Elizabeth Thompson
David Alexander Harris
<<<<<<< HEAD
Emily Louise Mitchell
=======
Emily Jane Mitchell
>>>>>>> dev
Christopher Daniel Evans
Amanda Nicole Foster
Matthew Thomas Carter
Olivia Rose Bennett
This shows how a line has been modified to “Emily Louise Mitchell” in the current branch (ours) and to “Emily Jane Mitchell” in the incoming branch (theirs). There is no indication here of the original (base) value.
Conflict marker styles
Git can also be configured to use a few different styles for these conflict markers. What you see above in the default merge conflict style but there are also diff3
and zdiff3
styles. These also add the base (i.e. the original) content inside the conflict marker, like so:
Michael James Anderson
Sarah Elizabeth Thompson
David Alexander Harris
<<<<<<< HEAD
Emily Louise Mitchell
||||||| d78303c
Emily Grace Mitchell
=======
Emily Jane Mitchell
>>>>>>> dev
Christopher Daniel Evans
Amanda Nicole Foster
Matthew Thomas Carter
Olivia Rose Bennett
In this example Git has been configured to use
zdiff3
and it shows that the original (base) value was “Emily Grace Mitchell”.
Resolving conflict markers
In a basic real-world conflict resolution scenario, the user would inspect this file and its remaining conflict markers, choose how to resolve each one, and manually edit the file to the desired final state by replacing the entire conflict section with whatever content the user thinks should remain, save the file and finalize the merge.
Performing manual text-based resolution of conflict sections is simple in principle, but not very ergonomic due to:
- Complete lack of visual aids such as color highlighting of the changes
- Vertically stacked layout makes it difficult to visually compare the changed lines
- No assistive “shortcut” actions to choose one side or another - everything must be done by manual editing
Existing tools
Some tools attempt to provide some assistance to help. For example, Visual Studio Code highlights conflict markers with some colors and provides code lens actions to accept a given side. Here’s the same zdiff3
style conflict section we saw above rendered in Visual Studio Code:
We want to provide something similar to this to our user, but implemented as an extension for CodeMirror, and with more visual clarity and even better ergonomics.
Not a three-way merge editor
As mentioned, what we are proposing is different from using a three way merge editor.
A full-fledged merge editor such as Beyond Compare or Kaleidoscope will typically disregard the conflict markers left by Git in the target file, and instead extract the ours, theirs and base versions from Git, and perform a three-way merge from scratch based on these three files, and then overwrite the target file with the resulting merged file.
In the scenario we want to target, by contrast, Git has already performed the merge of the file is question, and the file already contains the changes from both sides that did not conflict (let’s call these the non-conflicting changes ) along with conflict sections for anything Git was not able to resolve automatically.
We simply want to visualize those remaining conflict sections, and provide actions for a user to interact with them. There is only a single file to consider, and only a single editor is needed to resolve the remaining conflicts inside it.
Proposed Design
In a nutshell we are proposing an extension that turns this…
into this:
Basic principle
- Use a multi-line regex to identify conflict markers in the document and extract the relevant fragments
- Note: I’ve seen that there is a
placeholder
utility but I recall seeing that it can only be used to match strings in a single line. Do similar utilities exist to match over multiple lines? Is it reasonably easy to do, or would this be a significant obstacle to overcome?
- Note: I’ve seen that there is a
- Replace each such section with a block-level widget that shows the change versions side-by-side horizontally
- Use diff utilities in the
@codemirror/merge
package to highlight inline differences in ours and theirs compared to base - Disallow modifications inside the widget, but allow selection
- Provide actions to accept either one of the three versions, i.e. replace the entire conflict section with that version
- Provide actions to copy either one of the three versions to the clipboard
- Provide an action to dismiss the conflict, i.e. simply delete the conflict section from the document
No additional editor state
One of the main drivers behind proposing this particular design is that it would (presumably) not require any additional editor state. The current document content is sufficient at all times to represent the current state. The extension is only a visualization of the current content, along with some actions to mutate that content.
The normal document history and undo/redo actions can be used to get back to previous states, such as restore a conflict section that was accepted or dismissed by mistake.
Mapping
Once matched by the regex, here is how the pieces of a conflict section would be mapped to the rendered widget:
The strings “ours”, “original” and “theirs” should be used by default, but the host application should also be able to specify those in options when needed. Similarly, the refs are taken from the conflict marker by default, but also those can be overridden as options by the host application.
This enables scenarios where the sides of a conflict might be more recognizable by the user if they are expressed in terms of some higher-level application-defined concept.
Layout
The three versions of the conflicted section (ours , base and theirs ) are stacked side-by-side from left to right horizontally, each in one pane.
The horizontal layout significantly improves ergonomics and reduces the cognitive load required to compare the sides of the conflict, because the different versions of a modified line are always adjacent (unlike when the sides are presented vertically and two versions of the same line can be potentially dozens of lines apart, depending on conflict section length).
Each version pane always occupies one third of the available horizontal space, such that the versions are always equally wide. If their content contains lines longer than the available space, each pane scrolls horizontally to accommodate the overflow. (Respecting the word-wrapping configuration of the surrounding editor would also be acceptable, but not worth much additional effort or complexity.)
The horizontal layout is also much more scalable. Conflict sections can consist of any number of lines; the widget can simply grow vertically to accommodate, and each version pane grows accordingly.
Highlighting diff between versions
As mentioned above, the rendered widget should be able to use the existing utilities in the @codemirror/merge
package to highlight inline differences in ours and theirs compared to base as illustrated here:
Base version is optional
The default conflict marker style produced by git merge
has no base version. The diff3
and zdiff3
formats both include the base version and their formats are exactly the same; they differ only in what ends up inside the base version in some cases.
This proposed extension should handle all three styles, i.e. should handle the obsense of a base version gracefully. This means the regex used to match conflict sections needs to have the base section as optional, and the middle pane titled “original” can be missing. In such cases:
- The ours and theirs panes each occupy 50% of the available horizontal space
- The diff highlight can either be done between ours and theirs or disabled altogether
Interactivity
The rendered widget provides several points of user interaction, as illustrated here:
Coexistence with other extensions
Syntax highlighting
The highligting done by this extension is deliberately limited to background colors. This is to not interfere with any syntax highlighting that might simultaneously be enabled.
Unified merge view
It should be possible to enable this proposed extension together with the unifiedMergeView
extension (provided in the @codemirror/merge
package) on the same editor view. The unifiedMergedView
extension can then be configured with the base version of the document (which would normally be extracted from Git separately) as the original document. As a result, non-conflicting changes in the document (i.e. those changes that Git has already managed to automatically resolve) would be visualized as well.
By composing the two extensions in this way, we get a reasonably full-featured conflict resolution experience, without the need to implement a three-way merge editor or any merging algorithms.
Undo/redo
As already mentioned, the actions provided by this extensions should all be single-transaction mutations that integrate naturally with the normal undo/redo functionality. The user can then linearly regret any actions taken (e.g. accepting resolutions or dismissing conflicts) and the hosting application can optionally show UI for this outside the editor view itself.
Residence
In our opinion, this new proposed extension would fit very naturally in the @codemirror/merge
package, because:
- It is closely related to merge functionality
- It would use some existing functionality in this package (the diff utility functions)
- It would compose nicely together with the
unifiedMergeView
extension already provided by this package
Feasible or not?
So what does everyone think? Does this proposed design make sense, and is it feasible from an implementation standpoint?
Any feedback is highly appreciated.