mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 05:41:36 +00:00
More progress
This commit is contained in:
parent
6d7c0c8772
commit
9f62324c2a
5 changed files with 401 additions and 81 deletions
|
|
@ -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<DiffViewProps> = ({
|
||||
oldText,
|
||||
newText,
|
||||
onResolve,
|
||||
app,
|
||||
}) => {
|
||||
const DiffView: React.FC<DiffViewProps> = ({ oldText, newText, onResolve }) => {
|
||||
const [originalEditorView, setOriginalEditorView] =
|
||||
useState<EditorView | null>(null);
|
||||
const [modifiedEditorView, setModifiedEditorView] =
|
||||
useState<EditorView | null>(null);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorViewsRef = useRef<EditorView[]>([]);
|
||||
|
||||
|
|
@ -92,6 +94,12 @@ const DiffView: React.FC<DiffViewProps> = ({
|
|||
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<DiffViewProps> = ({
|
|||
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<DiffViewProps> = ({
|
|||
>
|
||||
<div
|
||||
className="original-editor"
|
||||
style={{ border: "1px solid var(--background-modifier-border)" }}
|
||||
style={{
|
||||
border: "1px solid var(--background-modifier-border)",
|
||||
backgroundColor: "var(--background-primary)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="diff-overlay"
|
||||
style={{
|
||||
gridRow: "1 / 2",
|
||||
gridColumn: "2 / 3",
|
||||
position: "relative",
|
||||
width: "50px",
|
||||
}}
|
||||
>
|
||||
<DiffConnections
|
||||
differences={diff(oldText, newText)}
|
||||
originalEditor={originalEditorView}
|
||||
modifiedEditor={modifiedEditorView}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="modified-editor"
|
||||
style={{ border: "1px solid var(--background-modifier-border)" }}
|
||||
style={{
|
||||
border: "1px solid var(--background-modifier-border)",
|
||||
backgroundColor: "var(--background-primary)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="result-editor"
|
||||
style={{
|
||||
gridColumn: "1 / span 2",
|
||||
gridColumn: "1 / 4",
|
||||
border: "1px solid var(--background-modifier-border)",
|
||||
backgroundColor: "var(--background-primary)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
273
src/views/conflicts-resolution/diff-connections.tsx
Normal file
273
src/views/conflicts-resolution/diff-connections.tsx
Normal file
|
|
@ -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<DiffConnectionProps> = ({
|
||||
differences,
|
||||
originalEditor,
|
||||
modifiedEditor,
|
||||
}) => {
|
||||
const [connections, setConnections] = useState<ConnectionChunk[]>([]);
|
||||
|
||||
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 (
|
||||
<svg
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{connections.map((chunk, i) => {
|
||||
const color = getConnectionStyle(chunk.type);
|
||||
return (
|
||||
<g key={i}>
|
||||
<path
|
||||
d={`
|
||||
M 0 ${chunk.startTop}
|
||||
C 20 ${chunk.startTop}, 30 ${chunk.endTop}, 50 ${chunk.endTop}
|
||||
L 50 ${chunk.endBottom}
|
||||
C 30 ${chunk.endBottom}, 20 ${chunk.startBottom}, 0 ${chunk.startBottom}
|
||||
Z
|
||||
`}
|
||||
fill={color}
|
||||
fillOpacity="0.1"
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<ConnectionButtons
|
||||
chunk={chunk}
|
||||
onAction={(action) => {
|
||||
console.log("Action:", action, "for chunk:", chunk);
|
||||
// TODO: Implement actual resolution actions
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<foreignObject x="0" y={centerY - 8} width="50" height="16">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent:
|
||||
showLeftArrow && showRightArrow
|
||||
? "space-between"
|
||||
: showLeftArrow
|
||||
? "flex-end"
|
||||
: "flex-start",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{showRightArrow && (
|
||||
<div
|
||||
style={{ cursor: "pointer", width: 16, height: 16 }}
|
||||
onClick={() => onAction("right")}
|
||||
ref={(node) => node && setIcon(node, "arrow-right")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showLeftArrow && (
|
||||
<div
|
||||
style={{ cursor: "pointer", width: 16, height: 16 }}
|
||||
onClick={() => onAction("left")}
|
||||
ref={(node) => node && setIcon(node, "arrow-left")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</foreignObject>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiffConnections;
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ export class ConflictsResolutionView extends ItemView {
|
|||
<DiffView
|
||||
oldText={oldText1}
|
||||
newText={newText1}
|
||||
app={this.app}
|
||||
onResolve={(text) => console.log("Resolved:", text)}
|
||||
// registerExtension={(ext) => this.plugin.registerEditorExtension(ext)}
|
||||
/>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue