Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Merge Conflict Ui#356

Open
fat wants to merge 11 commits intomainfrom
fat/merge-conflict
Open

Merge Conflict Ui#356
fat wants to merge 11 commits intomainfrom
fat/merge-conflict

Conversation

@fat
Copy link
Contributor

@fat fat commented Feb 25, 2026

Lots of people asking for this…

CleanShot.2026-02-24.at.22.31.12.mp4

@vercel
Copy link

vercel bot commented Feb 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierrejs-diff-demo Ready Ready Preview Mar 3, 2026 1:03am
pierrejs-docs Ready Ready Preview Mar 3, 2026 1:03am

Request Review

@amadeus
Copy link
Member

amadeus commented Feb 25, 2026

can you write a bit about the architecture on this? to help me review it better?

@fat
Copy link
Contributor Author

fat commented Mar 3, 2026

Alright here's how this works.



PR adds conflict resolution detection, helpers, and ui to file rendering.


## Highlevel API





You render a file with standard git conflict markers (<<<<<<, =======, etc.)





You get these options on file api:





mergeConflictActions:  ‘default’ | ‘none’ | (conflict, instance) => HTMLElement



 - by default we render a vscode-like api (Accept current change │ Accept incoming change │ Accept both)



 - you can also opt out of action bar w/ ‘none’


 
- you can also pass a custom thing and render your own ui





onMergeConflictAction => undefined: 
  - event delegation handler for click any of the actions rendered in the action bar.
  - This is optional, if you don’t supply we just resolve the file for you. If you do supply, then you need to manage. updating the contents yourself
  - note: this is only ever called if you do the default render path above (not custom).

## Conflict Detection

happens in getMergeConflictLineTypes.ts.

  1. It scans file lines once, top-to-bottom.
  2. It uses marker regexes:

  - <<<<<<< start
  - ||||||| base (optional, 3-way conflicts)
  - ======= separator
  - >>>>>>> end

  3. It keeps a stack of active conflicts (MergeConflictFrame) so nested conflicts work.

  - On start marker: push frame, stage=current
  - On base marker: stage=base
  - On separator: stage=incoming
  - On end marker: pop frame and finalize one MergeConflictRegion

  4. While inside a frame, non-marker lines are tagged by current stage:

  - current, base, or incoming
  - Marker lines are tagged as marker-start/base/separator/end
  - Lines outside conflicts are none

  5. When finalizing a region, it stores:

  - conflictIndex (0-based, parse order)
  - startLineIndex, separatorLineIndex, endLineIndex
  - optional baseMarkerLineIndex
  - line-number versions (+1) too

  6. Action row placement uses:

  - getMergeConflictActionLineNumber(conflict) = max(1, startLineNumber - 1)
    in the same file.

  If markers are malformed (for example missing separator/end), it won’t finalize a region for that frame.
  
## Interaction wiring (File component)

  - Default buttons use data-merge-conflict-action + data-merge-conflict-index.
  - File listens for clicks, validates resolution, resolves the region by conflictIndex, then
    emits payload.
    
## State ownership (controlled vs uncontrolled)

  - Controlled: if onMergeConflictAction exists, File calls it and does not mutate content
    internally.
  - Uncontrolled: File applies resolveMergeConflict(...) itself, then keeps an internal override
    (getEffectiveFile) until caller adopts/changes external content.
    

 Custom action UIs are slot-based (mergeConflictActions function + slot names) and managed via a
  small cache keyed by conflict slot name/region equality.



```

@@ -0,0 +1,35 @@
import type { PreloadFileOptions } from '@pierre/diffs/ssr';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simple conflict example…

</ButtonGroup>
</div>

<File
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conflicts are auto detected, ui is auto displayed, and conflict resolution is auto handled (similar to hunk divider logic)

this.options = { ...this.options, ...options };
}

private getInteractionManagerOptions(options: FileOptions<LAnnotation>) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup/cleanup weren't provided by default interaction options, so we just set them to our conflict listener actions

};
}

private getFileRendererOptions(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is annoying, but basically is for the fancy mergeConflictActions

return;
}

const conflictIndex = Number.parseInt(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

each mergeConflict instance has an index. We look up when index is firing here to make sure we resolve the correct region

event.preventDefault();

const payload = { resolution, conflict };
if (this.options.onMergeConflictAction != null) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if people provide this, we let them decide what to do… otherwise just resolve internally

return true;
}

private getEffectiveFile(file: FileContents): FileContents {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're managing the file state in a controlled way, we make decisions on what content to render here

};
}

private resolveMergeConflictInternally(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle the merge conflict (default)

}
this.annotationCache.clear();

for (const { element } of this.mergeConflictActionCache.values()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic matches annotaitonCache above

return lineCache.lines;
}

public getMergeConflictRegions(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helpers for working with mergeconflicts…

rowCount++;

if (mergeConflictActionType !== 'none') {
const mergeConflictActions =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create render ui and inject it

this.file,
renderRange
);
for (const conflict of conflicts) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append the slots for each merge conflict

@@ -0,0 +1,114 @@
import type { MergeConflictRegion } from '../types';

export type MergeConflictLineType =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bunch of parsing logic for merge conflicts, etc.

color-mix(in lab, var(--diffs-bg-context) 60%, var(--diffs-bg))
)
);
--diffs-bg-conflict-marker: var(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styles for merge conflict markers, etc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants