diff --git a/src/views/conflicts-resolution/unified-diff-view.tsx b/src/views/conflicts-resolution/unified-diff-view.tsx index 3d03daa..eb5229a 100644 --- a/src/views/conflicts-resolution/unified-diff-view.tsx +++ b/src/views/conflicts-resolution/unified-diff-view.tsx @@ -1,23 +1,7 @@ import * as React from "react"; -import CodeMirror, { - EditorState, - RangeSetBuilder, - StateEffect, - StateField, -} from "@uiw/react-codemirror"; -import { unifiedMergeView } from "@codemirror/merge"; -import { - Decoration, - DecorationSet, - EditorView, - GutterMarker, - keymap, - ViewPlugin, - ViewUpdate, - WidgetType, -} from "@codemirror/view"; -import { ChangeSet, Prec, RangeSet, Text } from "@codemirror/state"; -import { markdown } from "@codemirror/lang-markdown"; +import CodeMirror, { RangeSetBuilder, StateField } from "@uiw/react-codemirror"; +import { Decoration, EditorView } from "@codemirror/view"; + import diff, { DiffChunk } from "./diff"; const styles = document.createElement("style"); @@ -170,152 +154,21 @@ const createUnifiedDocument = ( return { doc: result.join("\n"), mapping: lineMappings }; }; -const getLineDecoration = (mapping: LineMapping) => { - if (mapping.oldLine !== null && mapping.newLine !== null) { - // No decoration for this case - return null; - } - - if (mapping.oldLine === null) { - // The line comes from the new document, it's an addition - return Decoration.line({ class: "cm-addedLine" }); - } else if (mapping.newLine === null) { - // The line comes from the old document, it's a deletion - return Decoration.line({ class: "cm-deletedLine" }); - } - return null; -}; - -function findMappingsRanges(mappings: LineMapping[]): number[][] { - const ranges: number[][] = []; - let currentRange: number[] | null = null; - - for (const mapping of mappings) { - // If either oldLine or newLine is null - if (mapping.oldLine === null || mapping.newLine === null) { - if (!currentRange) { - currentRange = [mapping.finalLine, mapping.finalLine]; - } else { - currentRange[1] = mapping.finalLine; - } - } else if (currentRange) { - ranges.push(currentRange); - currentRange = null; - } - } - - if (currentRange) { - ranges.push(currentRange); - } - - // Return the first range found - return ranges; -} - -const buildDecorations = (mappings: LineMapping[]) => { - return ViewPlugin.fromClass( - class { - decorations: DecorationSet; - mappings: LineMapping[]; - - constructor(view: EditorView) { - this.mappings = mappings; - this.decorations = this.buildDecorations(view); - } - - update(update: ViewUpdate) { - if (!update.docChanged) { - // Ignore non text changes - return; - } - // console.log("Previous selection\n", update.startState.selection); - // console.log("Current selection\n", update.state.selection); - const ranges = findMappingsRanges(this.mappings); - const affectedRanges = ranges.filter((range) => { - return ( - update.changes.touchesRange( - update.startState.doc.line(range[0]).from, - update.startState.doc.line(range[1]).to, - ) !== false - ); - }); - console.log(affectedRanges); - // update.changes.touchesRange(from); - - update.changes.iterChanges( - ( - fromA: number, - toA: number, - fromB: number, - toB: number, - inserted: Text, - ) => { - // update.startState.doc.lineAt(fromA); - // update.startState.doc.lineAt(toA); - // console.log("fromA", fromA); - // console.log("toA", toA); - // console.log("fromB", fromB); - // console.log("toB", toB); - // console.log("inserted", inserted); - }, - ); - this.decorations = this.buildDecorations(update.view); - } - - buildDecorations(view: EditorView): DecorationSet { - const decorations = []; - const doc = view.state.doc; - for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber++) { - const mapping = this.mappings.find((m) => m.finalLine === lineNumber); - if (!mapping) { - // Unlikely - continue; - } - const deco = getLineDecoration(mapping); - if (deco) { - const line = doc.line(lineNumber); - decorations.push(deco.range(line.from)); - } - } - return Decoration.set(decorations, true); - } - }, - { decorations: (v) => v.decorations }, - ); -}; - -interface ConflictRange { - from: number; - to: number; - source: "old" | "new" | "both"; // where the content originated -} - -const UnifiedDiffView: React.FC = ({ - initialOldText, - initialNewText, - onConflictResolved, -}) => { - const diffChunks = diff(initialOldText, initialNewText); - const { doc: unified, mapping } = createUnifiedDocument( - initialOldText, - initialNewText, - diffChunks, - ); - - const unifiedLines = unified.split("\n"); - - const initialRanges: ConflictRange[] = mapping.map((m) => { +const createInitialRanges = ( + doc: string, + mappings: LineMapping[], +): ConflictRange[] => { + const lines = doc.split("\n"); + return mappings.map((m) => { // Calculate line positions directly from text - const lineStartPos = unifiedLines - .slice(0, m.finalLine - 1) - .join("\n").length; + const lineStartPos = lines.slice(0, m.finalLine - 1).join("\n").length; // Add 1 for the newline except for first line - const fromPos = m.finalLine > 1 ? lineStartPos + 1 : 0; - const toPos = fromPos + unifiedLines[m.finalLine - 1].length; + const from = m.finalLine > 1 ? lineStartPos + 1 : 0; + const to = from + lines[m.finalLine - 1].length; return { - from: fromPos, - to: toPos, + from, + to, source: m.oldLine !== null && m.newLine !== null ? "both" @@ -324,8 +177,12 @@ const UnifiedDiffView: React.FC = ({ : "new", }; }); +}; - const conflictRangesField = StateField.define({ +const createRangesStateField = ( + initialRanges: ConflictRange[], +): StateField => { + return StateField.define({ create: () => initialRanges, update: (ranges, tr) => { if (!tr.docChanged) { @@ -348,41 +205,64 @@ const UnifiedDiffView: React.FC = ({ return newRanges; }, }); +}; - const buildDecorationsAlternate = EditorView.decorations.compute( - ["doc", conflictRangesField], - (state) => { - const ranges = state.field(conflictRangesField); - const builder = new RangeSetBuilder(); +const createDecorationsExtension = ( + rangesStateField: StateField, +) => { + return EditorView.decorations.compute(["doc", rangesStateField], (state) => { + const ranges = state.field(rangesStateField); + const builder = new RangeSetBuilder(); - for (const range of ranges) { - const startLine = state.doc.lineAt(range.from); - const endLine = state.doc.lineAt(range.to); - for (let i = 0; i <= endLine.number - startLine.number; i += 1) { - const line = state.doc.line(startLine.number + i); - if (range.source === "old") { - builder.add( - line.from, - line.from, - Decoration.line({ class: "cm-deletedLine" }), - ); - } else if (range.source === "new") { - builder.add( - line.from, - line.from, - Decoration.line({ class: "cm-addedLine" }), - ); - } + for (const range of ranges) { + const startLine = state.doc.lineAt(range.from); + const endLine = state.doc.lineAt(range.to); + for (let i = 0; i <= endLine.number - startLine.number; i += 1) { + const line = state.doc.line(startLine.number + i); + if (range.source === "old") { + builder.add( + line.from, + line.from, + Decoration.line({ class: "cm-deletedLine" }), + ); + } else if (range.source === "new") { + builder.add( + line.from, + line.from, + Decoration.line({ class: "cm-addedLine" }), + ); } } + } - return builder.finish(); - }, + return builder.finish(); + }); +}; + +interface ConflictRange { + from: number; + to: number; + source: "old" | "new" | "both"; // where the content originated +} + +const UnifiedDiffView: React.FC = ({ + initialOldText, + initialNewText, + onConflictResolved, +}) => { + const diffChunks = diff(initialOldText, initialNewText); + const { doc, mapping } = createUnifiedDocument( + initialOldText, + initialNewText, + diffChunks, ); + const initialRanges = createInitialRanges(doc, mapping); + const conflictRangesField = createRangesStateField(initialRanges); + const extensions = [ conflictRangesField, - buildDecorationsAlternate, + createDecorationsExtension(conflictRangesField), EditorView.editable.of(true), EditorView.theme({ "&": { @@ -410,8 +290,7 @@ const UnifiedDiffView: React.FC = ({ return (