mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 12:10:28 +00:00
Refactor to be more concise
This commit is contained in:
parent
63f9afe738
commit
5fd4c94566
6 changed files with 527 additions and 311 deletions
63
src/views/conflicts-resolution/actions-gutter.tsx
Normal file
63
src/views/conflicts-resolution/actions-gutter.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { DiffChunk } from "./diff";
|
||||||
|
|
||||||
|
interface ActionsGutterProps {
|
||||||
|
diffChunks: DiffChunk[];
|
||||||
|
// This is essential to correctly draw the lines between
|
||||||
|
// the left and right editor
|
||||||
|
lineHeight: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionsGutter: React.FC<ActionsGutterProps> = ({
|
||||||
|
diffChunks,
|
||||||
|
lineHeight,
|
||||||
|
width,
|
||||||
|
}) => {
|
||||||
|
const drawChunk = (chunk: DiffChunk, index: number) => {
|
||||||
|
const topLeft = (chunk.startLeftLine - 1) * lineHeight;
|
||||||
|
const bottomLeft = (chunk.endLeftLine - 1) * lineHeight;
|
||||||
|
const topRight = (chunk.startRightLine - 1) * lineHeight;
|
||||||
|
const bottomRight = (chunk.endRightLine - 1) * lineHeight;
|
||||||
|
const color =
|
||||||
|
chunk.type == "add"
|
||||||
|
? "var(--color-green)"
|
||||||
|
: chunk.type == "remove"
|
||||||
|
? "var(--color-red)"
|
||||||
|
: "var(--color-yellow)";
|
||||||
|
return (
|
||||||
|
<g key={index}>
|
||||||
|
<path
|
||||||
|
d={`
|
||||||
|
M 0 ${topLeft}
|
||||||
|
C ${width * 0.4} ${topLeft}, ${width * 0.6} ${topRight}, ${width} ${topRight}
|
||||||
|
L ${width} ${bottomRight}
|
||||||
|
C ${width * 0.6} ${bottomRight}, ${width * 0.4} ${bottomLeft}, 0 ${bottomLeft}
|
||||||
|
Z
|
||||||
|
`}
|
||||||
|
fill={color}
|
||||||
|
fillOpacity="0.1"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: width }}>
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
overflow: "visible",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{diffChunks.map(drawChunk)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionsGutter;
|
||||||
|
|
@ -5,6 +5,9 @@ import { markdown } from "@codemirror/lang-markdown";
|
||||||
import diff from "./diff";
|
import diff from "./diff";
|
||||||
import { createDiffHighlightPlugin } from "./diff-highlight-plugin";
|
import { createDiffHighlightPlugin } from "./diff-highlight-plugin";
|
||||||
import DiffConnections, { ConnectionChunk } from "./diff-connections";
|
import DiffConnections, { ConnectionChunk } from "./diff-connections";
|
||||||
|
import EditorPane from "./editor-pane";
|
||||||
|
import ActionsGutter from "./actions-gutter";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
// Add styles for diff highlighting
|
// Add styles for diff highlighting
|
||||||
const styles = document.createElement("style");
|
const styles = document.createElement("style");
|
||||||
|
|
@ -24,254 +27,315 @@ document.head.appendChild(styles);
|
||||||
interface DiffViewProps {
|
interface DiffViewProps {
|
||||||
oldText: string;
|
oldText: string;
|
||||||
newText: string;
|
newText: string;
|
||||||
onResolve: (resolvedText: string) => void;
|
onOldTextChange: (content: string) => void;
|
||||||
|
onNewTextChange: (content: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiffView: React.FC<DiffViewProps> = ({ oldText, newText, onResolve }) => {
|
const DiffView: React.FC<DiffViewProps> = ({
|
||||||
const [originalEditorView, setOriginalEditorView] =
|
oldText,
|
||||||
useState<EditorView | null>(null);
|
newText,
|
||||||
const [modifiedEditorView, setModifiedEditorView] =
|
onOldTextChange,
|
||||||
useState<EditorView | null>(null);
|
onNewTextChange,
|
||||||
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
// We need to know the line height to correctly draw the ribbon between the left
|
||||||
const editorViewsRef = useRef<EditorView[]>([]);
|
// and right editor in the actions gutter
|
||||||
|
const [lineHeight, setLineHeight] = React.useState<number>(0);
|
||||||
useEffect(() => {
|
const handleEditorReady = (editor: EditorView) => {
|
||||||
if (!containerRef.current) return;
|
setLineHeight(editor.defaultLineHeight);
|
||||||
|
|
||||||
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());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const [leftWidth, setLeftWidth] = useState("33%");
|
||||||
|
const [gutterWidth, setGutterWidth] = useState(100);
|
||||||
|
// const [rightWidth, setRightWidth] = useState("33%");
|
||||||
|
|
||||||
|
const diffs = diff(oldText, newText);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr auto 1fr",
|
gridTemplateColumns: "1fr auto 1fr",
|
||||||
gridTemplateRows: "1fr 1fr",
|
gridTemplateRows: "1fr",
|
||||||
gap: "10px",
|
gap: "0px",
|
||||||
height: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
className="original-editor"
|
<EditorPane
|
||||||
style={{
|
content={oldText}
|
||||||
border: "1px solid var(--background-modifier-border)",
|
highlightPlugin={createDiffHighlightPlugin({
|
||||||
backgroundColor: "var(--background-primary)",
|
diff: diffs,
|
||||||
}}
|
isOriginal: true,
|
||||||
/>
|
})}
|
||||||
<div
|
onEditorUpdate={handleEditorReady}
|
||||||
className="diff-overlay"
|
onContentChange={onOldTextChange}
|
||||||
style={{
|
/>
|
||||||
gridRow: "1 / 2",
|
</div>
|
||||||
gridColumn: "2 / 3",
|
<div>
|
||||||
position: "relative",
|
<ActionsGutter
|
||||||
width: "50px",
|
diffChunks={diffs}
|
||||||
}}
|
lineHeight={lineHeight}
|
||||||
>
|
width={gutterWidth}
|
||||||
<DiffConnections
|
/>
|
||||||
differences={diff(oldText, newText)}
|
</div>
|
||||||
originalEditor={originalEditorView}
|
<div>
|
||||||
modifiedEditor={modifiedEditorView}
|
<EditorPane
|
||||||
onMerge={handleMerge}
|
content={newText}
|
||||||
|
highlightPlugin={createDiffHighlightPlugin({
|
||||||
|
diff: diffs,
|
||||||
|
isOriginal: false,
|
||||||
|
})}
|
||||||
|
onContentChange={onNewTextChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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;
|
export default DiffView;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ import {
|
||||||
Decoration,
|
Decoration,
|
||||||
DecorationSet,
|
DecorationSet,
|
||||||
EditorView,
|
EditorView,
|
||||||
|
ViewUpdate,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
import { DiffResult } from "./diff";
|
import { DiffChunk } from "./diff";
|
||||||
|
|
||||||
interface DiffHighlightPluginSpec {
|
interface DiffHighlightPluginSpec {
|
||||||
diff: DiffResult[];
|
diff: DiffChunk[];
|
||||||
isOriginal: boolean;
|
isOriginal: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,40 +21,47 @@ export function createDiffHighlightPlugin(spec: DiffHighlightPluginSpec) {
|
||||||
this.decorations = this.buildDecorations(view);
|
this.decorations = this.buildDecorations(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged) {
|
||||||
|
this.decorations = this.buildDecorations(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildDecorations(view: EditorView): DecorationSet {
|
buildDecorations(view: EditorView): DecorationSet {
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
const doc = view.state.doc;
|
const doc = view.state.doc;
|
||||||
|
|
||||||
for (let i = 1; i <= doc.lines; i++) {
|
for (let i = 1; i <= doc.lines; i++) {
|
||||||
const line = doc.line(i);
|
const line = doc.line(i);
|
||||||
const diffResult = spec.diff.find((d) => {
|
|
||||||
|
const diffResult = spec.diff.find((chunk) => {
|
||||||
if (spec.isOriginal) {
|
if (spec.isOriginal) {
|
||||||
return (
|
return (
|
||||||
d.oldValue === line.text ||
|
(chunk.type === "remove" || chunk.type === "modify") &&
|
||||||
(d.type === "remove" && d.value === line.text)
|
i >= chunk.startLeftLine &&
|
||||||
|
i < chunk.endLeftLine
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return d.type !== "remove" && d.value === line.text;
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
(chunk.type === "add" || chunk.type === "modify") &&
|
||||||
|
i >= chunk.startRightLine &&
|
||||||
|
i < chunk.endRightLine
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (diffResult && diffResult.type !== "equal") {
|
if (diffResult) {
|
||||||
let className = "";
|
const className =
|
||||||
if (diffResult.type === "modify") {
|
diffResult.type === "modify"
|
||||||
className = "diff-modify-background";
|
? "diff-modify-background"
|
||||||
} else if (diffResult.type === "add" && !spec.isOriginal) {
|
: diffResult.type === "add"
|
||||||
className = "diff-add-background";
|
? "diff-add-background"
|
||||||
} else if (diffResult.type === "remove" && spec.isOriginal) {
|
: "diff-remove-background";
|
||||||
className = "diff-remove-background";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (className) {
|
decorations.push(
|
||||||
decorations.push(
|
Decoration.line({
|
||||||
Decoration.line({
|
class: className,
|
||||||
class: className,
|
}).range(line.from),
|
||||||
}).range(line.from),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,70 @@
|
||||||
type DiffType = "add" | "remove" | "modify" | "equal";
|
export interface DiffChunk {
|
||||||
|
type: "add" | "remove" | "modify";
|
||||||
export interface DiffResult {
|
startLeftLine: number;
|
||||||
type: DiffType;
|
endLeftLine: number;
|
||||||
value: string;
|
startRightLine: number;
|
||||||
oldValue?: string; // For modifications, store both values
|
endRightLine: number;
|
||||||
newValue?: string;
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function diff(oldText: string, newText: string): DiffResult[] {
|
function diff(oldText: string, newText: string): DiffChunk[] {
|
||||||
const oldLines = oldText.split("\n");
|
const oldLines = oldText.split("\n");
|
||||||
const newLines = newText.split("\n");
|
const newLines = newText.split("\n");
|
||||||
const result: DiffResult[] = [];
|
const result: DiffChunk[] = [];
|
||||||
// This is an index in oldText
|
|
||||||
let position = 0;
|
|
||||||
|
|
||||||
// First pass: find exact matches and obvious modifications
|
|
||||||
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
|
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
|
||||||
const oldLine = oldLines[i];
|
const oldLine = oldLines[i];
|
||||||
const newLine = newLines[i];
|
const newLine = newLines[i];
|
||||||
|
|
||||||
if (!oldLine && newLine) {
|
if (!oldLine && newLine) {
|
||||||
// Pure addition
|
|
||||||
result.push({
|
result.push({
|
||||||
type: "add",
|
type: "add",
|
||||||
value: newLine,
|
startLeftLine: i + 1,
|
||||||
from: position,
|
endLeftLine: i + 1,
|
||||||
to: position + newLine.length,
|
startRightLine: i + 1,
|
||||||
|
endRightLine: i + 2,
|
||||||
});
|
});
|
||||||
} else if (oldLine && !newLine) {
|
} else if (oldLine && !newLine) {
|
||||||
// Pure removal
|
|
||||||
result.push({
|
result.push({
|
||||||
type: "remove",
|
type: "remove",
|
||||||
value: oldLine,
|
startLeftLine: i + 1,
|
||||||
from: position,
|
endLeftLine: i + 2,
|
||||||
to: position + oldLine.length,
|
startRightLine: i + 1,
|
||||||
|
endRightLine: i + 1,
|
||||||
});
|
});
|
||||||
} else if (oldLine === newLine) {
|
} 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({
|
result.push({
|
||||||
type: "modify",
|
type: "modify",
|
||||||
value: newLine,
|
startLeftLine: i + 1,
|
||||||
oldValue: oldLine,
|
endLeftLine: i + 2,
|
||||||
newValue: newLine,
|
startRightLine: i + 1,
|
||||||
from: position,
|
endRightLine: i + 2,
|
||||||
to: position + Math.max(oldLine.length, newLine.length),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldLineLength = oldLine ? oldLine.length : 0;
|
|
||||||
const newLineLength = newLine ? newLine.length : 0;
|
|
||||||
position += Math.max(oldLineLength, newLineLength) + 1;
|
|
||||||
}
|
}
|
||||||
|
// return result;
|
||||||
|
return mergeDiffs(result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
function mergeDiffs(chunks: DiffChunk[]): DiffChunk[] {
|
||||||
|
if (chunks.length <= 1) return chunks;
|
||||||
|
|
||||||
|
return chunks.reduce((merged: DiffChunk[], current) => {
|
||||||
|
const previous = merged[merged.length - 1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
(previous.endLeftLine === current.startLeftLine ||
|
||||||
|
previous.endRightLine === current.startRightLine)
|
||||||
|
) {
|
||||||
|
previous.endLeftLine = current.endLeftLine;
|
||||||
|
previous.endRightLine = current.endRightLine;
|
||||||
|
previous.type = "modify";
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.push({ ...current });
|
||||||
|
return merged;
|
||||||
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default diff;
|
export default diff;
|
||||||
|
|
|
||||||
71
src/views/conflicts-resolution/editor-pane.tsx
Normal file
71
src/views/conflicts-resolution/editor-pane.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
|
import { EditorState, StateEffect } from "@codemirror/state";
|
||||||
|
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface EditorPaneProps {
|
||||||
|
content: string;
|
||||||
|
highlightPlugin: ViewPlugin<any>;
|
||||||
|
onEditorUpdate?: (editor: EditorView) => void;
|
||||||
|
onContentChange: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditorPane: React.FC<EditorPaneProps> = ({
|
||||||
|
content,
|
||||||
|
highlightPlugin,
|
||||||
|
onEditorUpdate,
|
||||||
|
onContentChange,
|
||||||
|
}) => {
|
||||||
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
const editorState = EditorState.create({
|
||||||
|
doc: content,
|
||||||
|
extensions: [
|
||||||
|
// basicSetup minus line numbers
|
||||||
|
// EditorView.lineWrapping,
|
||||||
|
EditorView.editable.of(true),
|
||||||
|
highlightPlugin,
|
||||||
|
EditorView.theme({
|
||||||
|
"&": {
|
||||||
|
backgroundColor: "var(--background-primary)",
|
||||||
|
color: "var(--text-normal)",
|
||||||
|
},
|
||||||
|
".cm-content": {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
|
||||||
|
background: "var(--text-selection)",
|
||||||
|
},
|
||||||
|
"&.cm-focused .cm-cursor": {
|
||||||
|
borderLeftColor: "var(--text-normal)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
markdown(),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
onContentChange(update.state.doc.toString());
|
||||||
|
}
|
||||||
|
// We want to know when it updates in case the line height changes
|
||||||
|
onEditorUpdate?.(update.view);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const editor = new EditorView({
|
||||||
|
parent: editorRef.current,
|
||||||
|
state: editorState,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup
|
||||||
|
editor.destroy();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div ref={editorRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditorPane;
|
||||||
|
|
@ -3,25 +3,26 @@ import { Root, createRoot } from "react-dom/client";
|
||||||
import DiffView from "./component";
|
import DiffView from "./component";
|
||||||
import GitHubSyncPlugin from "src/main";
|
import GitHubSyncPlugin from "src/main";
|
||||||
import { PluginContext } from "../hooks";
|
import { PluginContext } from "../hooks";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
export const CONFLICTS_RESOLUTION_VIEW_TYPE = "conflicts-resolution-view";
|
export const CONFLICTS_RESOLUTION_VIEW_TYPE = "conflicts-resolution-view";
|
||||||
|
|
||||||
// Test Case 1: Simple line changes
|
// Test Case 1: Simple line changes
|
||||||
const oldText1 = `# My Document
|
let oldText1 = `# My Document
|
||||||
This is a test
|
This is a test
|
||||||
Some content here
|
Some content here
|
||||||
Some line
|
Some line
|
||||||
Another line
|
Another line
|
||||||
Final line`;
|
Final line`;
|
||||||
|
|
||||||
const newText1 = `# My Document
|
let newText1 = `# My Document
|
||||||
This is a modified test
|
This is a modified test
|
||||||
Some new content here
|
Some new content here
|
||||||
Some line
|
Some line
|
||||||
Final line`;
|
Final line`;
|
||||||
|
|
||||||
// Test Case 2: Markdown with formatting
|
// Test Case 2: Markdown with formatting
|
||||||
const oldText2 = `# Title
|
let oldText2 = `# Title
|
||||||
## Subtitle
|
## Subtitle
|
||||||
- List item 1
|
- List item 1
|
||||||
- List item 2
|
- List item 2
|
||||||
|
|
@ -29,7 +30,7 @@ const oldText2 = `# Title
|
||||||
**Bold text** and *italic* text
|
**Bold text** and *italic* text
|
||||||
Regular paragraph`;
|
Regular paragraph`;
|
||||||
|
|
||||||
const newText2 = `# Modified Title
|
let newText2 = `# Modified Title
|
||||||
## Subtitle
|
## Subtitle
|
||||||
- List item 1
|
- List item 1
|
||||||
- List item 2
|
- List item 2
|
||||||
|
|
@ -61,15 +62,22 @@ export class ConflictsResolutionView extends ItemView {
|
||||||
const container = this.containerEl.children[1];
|
const container = this.containerEl.children[1];
|
||||||
container.empty();
|
container.empty();
|
||||||
const root: Root = createRoot(container);
|
const root: Root = createRoot(container);
|
||||||
root.render(
|
const App = () => {
|
||||||
<PluginContext.Provider value={this.plugin}>
|
const [oldText, setOldText] = React.useState(oldText1);
|
||||||
<DiffView
|
const [newText, setNewText] = React.useState(newText1);
|
||||||
oldText={oldText1}
|
|
||||||
newText={newText1}
|
return (
|
||||||
onResolve={(text) => console.log("Resolved:", text)}
|
<PluginContext.Provider value={this.plugin}>
|
||||||
/>
|
<DiffView
|
||||||
</PluginContext.Provider>,
|
oldText={oldText}
|
||||||
);
|
newText={newText}
|
||||||
|
onOldTextChange={setOldText}
|
||||||
|
onNewTextChange={setNewText}
|
||||||
|
/>
|
||||||
|
</PluginContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
root.render(<App />);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClose() {
|
async onClose() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue