From 9f62324c2a2359f2075273404dae19026eca4013 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 13 Feb 2025 17:23:43 +0100 Subject: [PATCH] More progress --- src/views/conflicts-resolution/component.tsx | 60 +++- .../conflicts-resolution/diff-connections.tsx | 273 ++++++++++++++++++ .../diff-highlight-plugin.ts | 42 ++- src/views/conflicts-resolution/diff.ts | 106 +++---- src/views/conflicts-resolution/view.tsx | 1 - 5 files changed, 401 insertions(+), 81 deletions(-) create mode 100644 src/views/conflicts-resolution/diff-connections.tsx diff --git a/src/views/conflicts-resolution/component.tsx b/src/views/conflicts-resolution/component.tsx index 05d25e0..5dddcb0 100644 --- a/src/views/conflicts-resolution/component.tsx +++ b/src/views/conflicts-resolution/component.tsx @@ -1,20 +1,23 @@ -import { App } from "obsidian"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import { markdown } from "@codemirror/lang-markdown"; import diff from "./diff"; import { createDiffHighlightPlugin } from "./diff-highlight-plugin"; +import DiffConnections from "./diff-connections"; // Add styles for diff highlighting const styles = document.createElement("style"); styles.innerHTML = ` - .diff-remove-background { - background-color: rgba(var(--background-modifier-error-rgb), 0.1); + .diff-modify-background { + background-color: rgba(var(--color-yellow-rgb), 0.1); } .diff-add-background { background-color: rgba(var(--color-green-rgb), 0.1); } + .diff-remove-background { + background-color: rgba(var(--color-red-rgb), 0.1); + } `; document.head.appendChild(styles); @@ -22,15 +25,14 @@ interface DiffViewProps { oldText: string; newText: string; onResolve: (resolvedText: string) => void; - app: App; } -const DiffView: React.FC = ({ - oldText, - newText, - onResolve, - app, -}) => { +const DiffView: React.FC = ({ oldText, newText, onResolve }) => { + const [originalEditorView, setOriginalEditorView] = + useState(null); + const [modifiedEditorView, setModifiedEditorView] = + useState(null); + const containerRef = useRef(null); const editorViewsRef = useRef([]); @@ -92,6 +94,12 @@ const DiffView: React.FC = ({ parent: container, }); + if (isOriginal) { + setOriginalEditorView(view); + } else if (readOnly) { + setModifiedEditorView(view); + } + editorViewsRef.current.push(view); return view; }; @@ -128,7 +136,7 @@ const DiffView: React.FC = ({ ref={containerRef} style={{ display: "grid", - gridTemplateColumns: "1fr 1fr", + gridTemplateColumns: "1fr auto 1fr", gridTemplateRows: "1fr 1fr", gap: "10px", height: "100%", @@ -136,17 +144,39 @@ const DiffView: React.FC = ({ >
+
+ +
diff --git a/src/views/conflicts-resolution/diff-connections.tsx b/src/views/conflicts-resolution/diff-connections.tsx new file mode 100644 index 0000000..1e8c3f6 --- /dev/null +++ b/src/views/conflicts-resolution/diff-connections.tsx @@ -0,0 +1,273 @@ +import { setIcon } from "obsidian"; +import { useEffect, useState } from "react"; +import { EditorView } from "@codemirror/view"; +import { DiffResult } from "./diff"; + +interface DiffConnectionProps { + differences: DiffResult[]; + originalEditor: EditorView | null; + modifiedEditor: EditorView | null; +} + +type ConnectionType = "add" | "remove" | "modify"; + +type ConnectionChunk = { + startTop: number; + startBottom: number; + endTop: number; + endBottom: number; + type: ConnectionType; +}; + +const DiffConnections: React.FC = ({ + differences, + originalEditor, + modifiedEditor, +}) => { + const [connections, setConnections] = useState([]); + + useEffect(() => { + if (!originalEditor || !modifiedEditor) return; + + requestAnimationFrame(() => { + const originalContainer = + originalEditor.scrollDOM.getBoundingClientRect(); + const modifiedContainer = + modifiedEditor.scrollDOM.getBoundingClientRect(); + + // Group consecutive diffs into chunks + const chunks: ConnectionChunk[] = []; + let currentChunk: DiffResult[] = []; + + differences.forEach((diff, i) => { + if (diff.type === "equal") { + if (currentChunk.length > 0) { + // Process the chunk + const chunk = processChunk( + currentChunk, + originalEditor, + modifiedEditor, + originalContainer, + modifiedContainer, + ); + if (chunk) chunks.push(chunk); + currentChunk = []; + } + } else { + currentChunk.push(diff); + } + }); + + // Process last chunk if exists + if (currentChunk.length > 0) { + const chunk = processChunk( + currentChunk, + originalEditor, + modifiedEditor, + originalContainer, + modifiedContainer, + ); + if (chunk) chunks.push(chunk); + } + + setConnections(chunks); + }); + }, [differences, originalEditor, modifiedEditor]); + + const processChunk = ( + chunk: DiffResult[], + originalEditor: EditorView, + modifiedEditor: EditorView, + originalContainer: DOMRect, + modifiedContainer: DOMRect, + ): ConnectionChunk | null => { + let startTop = Infinity; + let startBottom = -Infinity; + let endTop = Infinity; + let endBottom = -Infinity; + + chunk.forEach((diff) => { + if (diff.type === "modify" || diff.type === "remove") { + // Find position in original editor + const line = findLineByContent( + originalEditor, + diff.oldValue || diff.value, + ); + if (line) { + startTop = Math.min(startTop, line.top - originalContainer.top); + startBottom = Math.max( + startBottom, + line.bottom - originalContainer.top, + ); + } + } + + if (diff.type === "modify" || diff.type === "add") { + // Find position in modified editor + const line = findLineByContent(modifiedEditor, diff.value); + if (line) { + endTop = Math.min(endTop, line.top - modifiedContainer.top); + endBottom = Math.max(endBottom, line.bottom - modifiedContainer.top); + } + } + }); + + if (startTop === Infinity && endTop === Infinity) return null; + + // For pure additions, position start at the previous equal line's bottom + if (startTop === Infinity) { + const prevEqual = findPreviousEqual(differences, chunk[0]); + if (prevEqual) { + const line = findLineByContent(originalEditor, prevEqual.value); + if (line) { + startTop = startBottom = line.bottom - originalContainer.top; + } + } + } + + return { + startTop: startTop === Infinity ? endTop : startTop, + startBottom: startBottom === -Infinity ? endBottom : startBottom, + endTop: endTop === Infinity ? startTop : endTop, + endBottom: endBottom === -Infinity ? startBottom : endBottom, + type: (chunk.some((d) => d.type === "modify") + ? "modify" + : chunk[0].type) as ConnectionType, + }; + }; + + return ( + + {connections.map((chunk, i) => { + const color = getConnectionStyle(chunk.type); + return ( + + + { + console.log("Action:", action, "for chunk:", chunk); + // TODO: Implement actual resolution actions + }} + /> + + ); + })} + + ); +}; + +const findLineByContent = ( + editor: EditorView, + content?: string, +): { top: number; bottom: number } | null => { + if (!content) return null; + + const doc = editor.state.doc; + for (let i = 1; i <= doc.lines; i++) { + const line = doc.line(i); + if (line.text === content) { + const fromCoords = editor.coordsAtPos(line.from); + const toCoords = editor.coordsAtPos(line.to); + if (fromCoords && toCoords) { + return { + top: fromCoords.top, + bottom: toCoords.bottom, + }; + } + } + } + return null; +}; + +const findPreviousEqual = ( + differences: DiffResult[], + currentDiff: DiffResult, +): DiffResult | null => { + const currentIndex = differences.findIndex((d) => d === currentDiff); + if (currentIndex === -1) return null; + + for (let i = currentIndex - 1; i >= 0; i--) { + if (differences[i].type === "equal") { + return differences[i]; + } + } + return null; +}; + +const getConnectionStyle = (type: ConnectionType): string => { + switch (type) { + case "add": + return "var(--color-green)"; + case "remove": + return "var(--color-red)"; + case "modify": + return "var(--color-yellow)"; + } +}; + +const ConnectionButtons: React.FC<{ + chunk: ConnectionChunk; + onAction: (action: "left" | "right") => void; +}> = ({ chunk, onAction }) => { + const centerY = + (chunk.startTop + chunk.startBottom + chunk.endTop + chunk.endBottom) / 4; + + const showLeftArrow = chunk.type === "modify" || chunk.type === "add"; + const showRightArrow = chunk.type === "modify" || chunk.type === "remove"; + + return ( + +
+ {showRightArrow && ( +
onAction("right")} + ref={(node) => node && setIcon(node, "arrow-right")} + /> + )} + + {showLeftArrow && ( +
onAction("left")} + ref={(node) => node && setIcon(node, "arrow-left")} + /> + )} +
+ + ); +}; + +export default DiffConnections; diff --git a/src/views/conflicts-resolution/diff-highlight-plugin.ts b/src/views/conflicts-resolution/diff-highlight-plugin.ts index 2c4f93a..311b899 100644 --- a/src/views/conflicts-resolution/diff-highlight-plugin.ts +++ b/src/views/conflicts-resolution/diff-highlight-plugin.ts @@ -24,26 +24,36 @@ export function createDiffHighlightPlugin(spec: DiffHighlightPluginSpec) { const decorations = []; const doc = view.state.doc; - // Go through the document line by line for (let i = 1; i <= doc.lines; i++) { const line = doc.line(i); - const lineText = line.text; + const diffResult = spec.diff.find((d) => { + if (spec.isOriginal) { + return ( + d.oldValue === line.text || + (d.type === "remove" && d.value === line.text) + ); + } else { + return d.type !== "remove" && d.value === line.text; + } + }); - // Find matching diff for this line - const diff = spec.diff.find((d) => d.value === lineText); + if (diffResult && diffResult.type !== "equal") { + let className = ""; + if (diffResult.type === "modify") { + className = "diff-modify-background"; + } else if (diffResult.type === "add" && !spec.isOriginal) { + className = "diff-add-background"; + } else if (diffResult.type === "remove" && spec.isOriginal) { + className = "diff-remove-background"; + } - if ( - diff && - ((spec.isOriginal && diff.type === "remove") || - (!spec.isOriginal && diff.type === "add")) - ) { - decorations.push( - Decoration.mark({ - class: spec.isOriginal - ? "diff-remove-background" - : "diff-add-background", - }).range(line.from, line.to), - ); + if (className) { + decorations.push( + Decoration.line({ + class: className, + }).range(line.from), + ); + } } } diff --git a/src/views/conflicts-resolution/diff.ts b/src/views/conflicts-resolution/diff.ts index 8a41d52..e45eee4 100644 --- a/src/views/conflicts-resolution/diff.ts +++ b/src/views/conflicts-resolution/diff.ts @@ -1,69 +1,77 @@ -export type DiffResult = { - type: "add" | "remove" | "equal"; +type DiffType = "add" | "remove" | "modify" | "equal"; + +export interface DiffResult { + type: DiffType; value: string; + oldValue?: string; // For modifications, store both values + newValue?: string; from: number; to: number; -}; +} -export default function diff(oldText: string, newText: string): DiffResult[] { +function diff(oldText: string, newText: string): DiffResult[] { const oldLines = oldText.split("\n"); const newLines = newText.split("\n"); - - const matrix = Array(oldLines.length + 1) - .fill(null) - .map(() => Array(newLines.length + 1).fill(0)); - - // Fill the matrix - for (let i = 1; i <= oldLines.length; i++) { - for (let j = 1; j <= newLines.length; j++) { - if (oldLines[i - 1] === newLines[j - 1]) { - matrix[i][j] = matrix[i - 1][j - 1] + 1; - } else { - matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); - } - } - } - - // Backtrack to find differences const result: DiffResult[] = []; - let i = oldLines.length; - let j = newLines.length; let position = 0; - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { - const value = oldLines[i - 1]; - result.unshift({ - type: "equal", - value, - from: position, - to: position + value.length, - }); - position += value.length + 1; // +1 for newline - i--; - j--; - } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { - const value = newLines[j - 1]; - result.unshift({ + // First pass: find exact matches and obvious modifications + for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) { + const oldLine = oldLines[i]; + const newLine = newLines[i]; + + if (!oldLine && newLine) { + // Pure addition + result.push({ type: "add", - value, + value: newLine, from: position, - to: position + value.length, + to: position + newLine.length, }); - position += value.length + 1; - j--; - } else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) { - const value = oldLines[i - 1]; - result.unshift({ + } else if (oldLine && !newLine) { + // Pure removal + result.push({ type: "remove", - value, + value: oldLine, from: position, - to: position + value.length, + to: position + oldLine.length, + }); + } else if (oldLine === newLine) { + // Exact match + result.push({ + type: "equal", + value: oldLine, + from: position, + to: position + oldLine.length, + }); + } else { + // Different content at same line number - treat as modification + result.push({ + type: "modify", + value: newLine, + oldValue: oldLine, + newValue: newLine, + from: position, + to: position + Math.max(oldLine.length, newLine.length), }); - position += value.length + 1; - i--; } + + position += + Math.max(oldLine ? oldLine.length : 0, newLine ? newLine.length : 0) + 1; } return result; } + +function similarity(s1: string, s2: string): number { + if (s1 === s2) return 1.0; + + // Simple word-based similarity + const words1 = s1.split(/\s+/); + const words2 = s2.split(/\s+/); + + const commonWords = words1.filter((w) => words2.includes(w)).length; + return commonWords / Math.max(words1.length, words2.length); +} + +export default diff; diff --git a/src/views/conflicts-resolution/view.tsx b/src/views/conflicts-resolution/view.tsx index ae10000..d994cc7 100644 --- a/src/views/conflicts-resolution/view.tsx +++ b/src/views/conflicts-resolution/view.tsx @@ -62,7 +62,6 @@ export class ConflictsResolutionView extends ItemView { console.log("Resolved:", text)} // registerExtension={(ext) => this.plugin.registerEditorExtension(ext)} />,