mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 12:10:28 +00:00
277 lines
7.6 KiB
TypeScript
277 lines
7.6 KiB
TypeScript
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, { ConnectionChunk } from "./diff-connections";
|
|
|
|
// Add styles for diff highlighting
|
|
const styles = document.createElement("style");
|
|
styles.innerHTML = `
|
|
.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);
|
|
|
|
interface DiffViewProps {
|
|
oldText: string;
|
|
newText: string;
|
|
onResolve: (resolvedText: string) => void;
|
|
}
|
|
|
|
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[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const createEditor = (
|
|
container: HTMLElement,
|
|
content: string,
|
|
readOnly: boolean = false,
|
|
isOriginal: boolean = false,
|
|
) => {
|
|
const differences = diff(oldText || "", newText || "");
|
|
|
|
const highlightPlugin = createDiffHighlightPlugin({
|
|
diff: differences,
|
|
isOriginal,
|
|
});
|
|
|
|
const changeListener = !readOnly
|
|
? [
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
onResolve(update.state.doc.toString());
|
|
}
|
|
}),
|
|
]
|
|
: [];
|
|
|
|
const editorState = EditorState.create({
|
|
doc: content,
|
|
extensions: [
|
|
// basicSetup minus line numbers
|
|
EditorView.lineWrapping,
|
|
EditorView.editable.of(!readOnly),
|
|
highlightPlugin,
|
|
EditorView.theme({
|
|
"&": {
|
|
backgroundColor: "var(--background-primary)",
|
|
color: "var(--text-normal)",
|
|
},
|
|
".cm-line": {
|
|
padding: "0 4px",
|
|
},
|
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
|
background: "var(--text-selection)",
|
|
},
|
|
"&.cm-focused .cm-cursor": {
|
|
borderLeftColor: "var(--text-normal)",
|
|
},
|
|
}),
|
|
markdown(),
|
|
...changeListener,
|
|
],
|
|
});
|
|
|
|
const view = new EditorView({
|
|
state: editorState,
|
|
parent: container,
|
|
});
|
|
|
|
if (isOriginal) {
|
|
setOriginalEditorView(view);
|
|
} else if (readOnly) {
|
|
setModifiedEditorView(view);
|
|
}
|
|
|
|
editorViewsRef.current.push(view);
|
|
return view;
|
|
};
|
|
|
|
// Create the three editors
|
|
const originalView = createEditor(
|
|
containerRef.current.querySelector(".original-editor")!,
|
|
oldText,
|
|
true,
|
|
true,
|
|
);
|
|
|
|
const modifiedView = createEditor(
|
|
containerRef.current.querySelector(".modified-editor")!,
|
|
newText,
|
|
true,
|
|
false,
|
|
);
|
|
|
|
const resultView = createEditor(
|
|
containerRef.current.querySelector(".result-editor")!,
|
|
newText,
|
|
false,
|
|
);
|
|
|
|
return () => {
|
|
editorViewsRef.current.forEach((view) => view.destroy());
|
|
editorViewsRef.current = [];
|
|
};
|
|
}, [oldText, newText]);
|
|
|
|
const handleMerge = (direction: "left" | "right", chunk: ConnectionChunk) => {
|
|
if (!originalEditorView || !modifiedEditorView) {
|
|
return;
|
|
}
|
|
|
|
const sourceEditor =
|
|
direction === "left" ? modifiedEditorView : originalEditorView;
|
|
const targetEditor =
|
|
direction === "left" ? originalEditorView : modifiedEditorView;
|
|
|
|
const sourceLine =
|
|
direction === "left" ? chunk.targetStartLine : chunk.startLine;
|
|
const sourceEndLine =
|
|
direction === "left" ? chunk.targetEndLine : chunk.endLine;
|
|
const targetLine =
|
|
direction === "left" ? chunk.startLine : chunk.targetStartLine;
|
|
const targetEndLine =
|
|
direction === "left" ? chunk.endLine : chunk.targetEndLine;
|
|
|
|
const sourceDoc = sourceEditor.state.doc;
|
|
const targetDoc = targetEditor.state.doc;
|
|
|
|
// Handle source content
|
|
let contentToInsert = "";
|
|
if (sourceLine > sourceDoc.lines) {
|
|
// Adding new lines at the end
|
|
contentToInsert = "\n" + chunk.content.join("\n");
|
|
} else {
|
|
const sourceFrom = sourceDoc.line(sourceLine).from;
|
|
const sourceTo =
|
|
sourceEndLine <= sourceDoc.lines
|
|
? sourceDoc.line(sourceEndLine).to
|
|
: sourceDoc.length;
|
|
contentToInsert = sourceEditor.state.sliceDoc(sourceFrom, sourceTo);
|
|
}
|
|
|
|
// Handle target position
|
|
let targetFrom = 0;
|
|
let targetTo = 0;
|
|
|
|
if (targetLine > targetDoc.lines) {
|
|
// Appending to end of file
|
|
targetFrom = targetTo = targetDoc.length;
|
|
if (!contentToInsert.startsWith("\n") && targetDoc.length > 0) {
|
|
contentToInsert = "\n" + contentToInsert;
|
|
}
|
|
} else {
|
|
targetFrom = targetDoc.line(targetLine).from;
|
|
targetTo =
|
|
targetEndLine <= targetDoc.lines
|
|
? targetDoc.line(targetEndLine).to
|
|
: targetDoc.length;
|
|
}
|
|
|
|
// Apply the change
|
|
const change = {
|
|
from: targetFrom,
|
|
to: targetTo,
|
|
insert: contentToInsert,
|
|
};
|
|
|
|
targetEditor.dispatch({ changes: change });
|
|
|
|
// Update result editor if it exists
|
|
// if (resultEditor) {
|
|
// const resultDoc = resultEditor.state.doc;
|
|
// let resultFrom = targetFrom;
|
|
// let resultTo = targetTo;
|
|
|
|
// // Handle case where result editor might have different content length
|
|
// if (resultFrom > resultDoc.length) {
|
|
// resultFrom = resultTo = resultDoc.length;
|
|
// if (!contentToInsert.startsWith("\n") && resultDoc.length > 0) {
|
|
// contentToInsert = "\n" + contentToInsert;
|
|
// }
|
|
// }
|
|
|
|
// resultEditor.dispatch({
|
|
// changes: {
|
|
// from: resultFrom,
|
|
// to: resultTo,
|
|
// insert: contentToInsert,
|
|
// },
|
|
// });
|
|
// }
|
|
|
|
// Notify parent
|
|
onResolve(targetEditor.state.doc.toString());
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr auto 1fr",
|
|
gridTemplateRows: "1fr 1fr",
|
|
gap: "10px",
|
|
height: "100%",
|
|
}}
|
|
>
|
|
<div
|
|
className="original-editor"
|
|
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}
|
|
onMerge={handleMerge}
|
|
/>
|
|
</div>
|
|
<div
|
|
className="modified-editor"
|
|
style={{
|
|
border: "1px solid var(--background-modifier-border)",
|
|
backgroundColor: "var(--background-primary)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="result-editor"
|
|
style={{
|
|
gridColumn: "1 / 4",
|
|
border: "1px solid var(--background-modifier-border)",
|
|
backgroundColor: "var(--background-primary)",
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DiffView;
|