mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 05:41:36 +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 { createDiffHighlightPlugin } from "./diff-highlight-plugin";
|
||||
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
|
||||
const styles = document.createElement("style");
|
||||
|
|
@ -24,254 +27,315 @@ document.head.appendChild(styles);
|
|||
interface DiffViewProps {
|
||||
oldText: string;
|
||||
newText: string;
|
||||
onResolve: (resolvedText: string) => void;
|
||||
onOldTextChange: (content: string) => void;
|
||||
onNewTextChange: (content: 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());
|
||||
const DiffView: React.FC<DiffViewProps> = ({
|
||||
oldText,
|
||||
newText,
|
||||
onOldTextChange,
|
||||
onNewTextChange,
|
||||
}) => {
|
||||
// We need to know the line height to correctly draw the ribbon between the left
|
||||
// and right editor in the actions gutter
|
||||
const [lineHeight, setLineHeight] = React.useState<number>(0);
|
||||
const handleEditorReady = (editor: EditorView) => {
|
||||
setLineHeight(editor.defaultLineHeight);
|
||||
};
|
||||
|
||||
// const [leftWidth, setLeftWidth] = useState("33%");
|
||||
const [gutterWidth, setGutterWidth] = useState(100);
|
||||
// const [rightWidth, setRightWidth] = useState("33%");
|
||||
|
||||
const diffs = diff(oldText, newText);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto 1fr",
|
||||
gridTemplateRows: "1fr 1fr",
|
||||
gap: "10px",
|
||||
height: "100%",
|
||||
gridTemplateRows: "1fr",
|
||||
gap: "0px",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<EditorPane
|
||||
content={oldText}
|
||||
highlightPlugin={createDiffHighlightPlugin({
|
||||
diff: diffs,
|
||||
isOriginal: true,
|
||||
})}
|
||||
onEditorUpdate={handleEditorReady}
|
||||
onContentChange={onOldTextChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ActionsGutter
|
||||
diffChunks={diffs}
|
||||
lineHeight={lineHeight}
|
||||
width={gutterWidth}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<EditorPane
|
||||
content={newText}
|
||||
highlightPlugin={createDiffHighlightPlugin({
|
||||
diff: diffs,
|
||||
isOriginal: false,
|
||||
})}
|
||||
onContentChange={onNewTextChange}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import {
|
|||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
ViewUpdate,
|
||||
} from "@codemirror/view";
|
||||
import { DiffResult } from "./diff";
|
||||
import { DiffChunk } from "./diff";
|
||||
|
||||
interface DiffHighlightPluginSpec {
|
||||
diff: DiffResult[];
|
||||
diff: DiffChunk[];
|
||||
isOriginal: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -20,40 +21,47 @@ export function createDiffHighlightPlugin(spec: DiffHighlightPluginSpec) {
|
|||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const decorations = [];
|
||||
const doc = view.state.doc;
|
||||
|
||||
for (let i = 1; i <= doc.lines; i++) {
|
||||
const line = doc.line(i);
|
||||
const diffResult = spec.diff.find((d) => {
|
||||
|
||||
const diffResult = spec.diff.find((chunk) => {
|
||||
if (spec.isOriginal) {
|
||||
return (
|
||||
d.oldValue === line.text ||
|
||||
(d.type === "remove" && d.value === line.text)
|
||||
(chunk.type === "remove" || chunk.type === "modify") &&
|
||||
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") {
|
||||
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 (diffResult) {
|
||||
const className =
|
||||
diffResult.type === "modify"
|
||||
? "diff-modify-background"
|
||||
: diffResult.type === "add"
|
||||
? "diff-add-background"
|
||||
: "diff-remove-background";
|
||||
|
||||
if (className) {
|
||||
decorations.push(
|
||||
Decoration.line({
|
||||
class: className,
|
||||
}).range(line.from),
|
||||
);
|
||||
}
|
||||
decorations.push(
|
||||
Decoration.line({
|
||||
class: className,
|
||||
}).range(line.from),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,68 +1,70 @@
|
|||
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 interface DiffChunk {
|
||||
type: "add" | "remove" | "modify";
|
||||
startLeftLine: number;
|
||||
endLeftLine: number;
|
||||
startRightLine: number;
|
||||
endRightLine: number;
|
||||
}
|
||||
|
||||
function diff(oldText: string, newText: string): DiffResult[] {
|
||||
function diff(oldText: string, newText: string): DiffChunk[] {
|
||||
const oldLines = oldText.split("\n");
|
||||
const newLines = newText.split("\n");
|
||||
const result: DiffResult[] = [];
|
||||
// This is an index in oldText
|
||||
let position = 0;
|
||||
const result: DiffChunk[] = [];
|
||||
|
||||
// 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: newLine,
|
||||
from: position,
|
||||
to: position + newLine.length,
|
||||
startLeftLine: i + 1,
|
||||
endLeftLine: i + 1,
|
||||
startRightLine: i + 1,
|
||||
endRightLine: i + 2,
|
||||
});
|
||||
} else if (oldLine && !newLine) {
|
||||
// Pure removal
|
||||
result.push({
|
||||
type: "remove",
|
||||
value: oldLine,
|
||||
from: position,
|
||||
to: position + oldLine.length,
|
||||
startLeftLine: i + 1,
|
||||
endLeftLine: i + 2,
|
||||
startRightLine: i + 1,
|
||||
endRightLine: i + 1,
|
||||
});
|
||||
} 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
|
||||
} else if (oldLine !== newLine) {
|
||||
result.push({
|
||||
type: "modify",
|
||||
value: newLine,
|
||||
oldValue: oldLine,
|
||||
newValue: newLine,
|
||||
from: position,
|
||||
to: position + Math.max(oldLine.length, newLine.length),
|
||||
startLeftLine: i + 1,
|
||||
endLeftLine: i + 2,
|
||||
startRightLine: i + 1,
|
||||
endRightLine: i + 2,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
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 GitHubSyncPlugin from "src/main";
|
||||
import { PluginContext } from "../hooks";
|
||||
import * as React from "react";
|
||||
|
||||
export const CONFLICTS_RESOLUTION_VIEW_TYPE = "conflicts-resolution-view";
|
||||
|
||||
// Test Case 1: Simple line changes
|
||||
const oldText1 = `# My Document
|
||||
let oldText1 = `# My Document
|
||||
This is a test
|
||||
Some content here
|
||||
Some line
|
||||
Another line
|
||||
Final line`;
|
||||
|
||||
const newText1 = `# My Document
|
||||
let newText1 = `# My Document
|
||||
This is a modified test
|
||||
Some new content here
|
||||
Some line
|
||||
Final line`;
|
||||
|
||||
// Test Case 2: Markdown with formatting
|
||||
const oldText2 = `# Title
|
||||
let oldText2 = `# Title
|
||||
## Subtitle
|
||||
- List item 1
|
||||
- List item 2
|
||||
|
|
@ -29,7 +30,7 @@ const oldText2 = `# Title
|
|||
**Bold text** and *italic* text
|
||||
Regular paragraph`;
|
||||
|
||||
const newText2 = `# Modified Title
|
||||
let newText2 = `# Modified Title
|
||||
## Subtitle
|
||||
- List item 1
|
||||
- List item 2
|
||||
|
|
@ -61,15 +62,22 @@ export class ConflictsResolutionView extends ItemView {
|
|||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
const root: Root = createRoot(container);
|
||||
root.render(
|
||||
<PluginContext.Provider value={this.plugin}>
|
||||
<DiffView
|
||||
oldText={oldText1}
|
||||
newText={newText1}
|
||||
onResolve={(text) => console.log("Resolved:", text)}
|
||||
/>
|
||||
</PluginContext.Provider>,
|
||||
);
|
||||
const App = () => {
|
||||
const [oldText, setOldText] = React.useState(oldText1);
|
||||
const [newText, setNewText] = React.useState(newText1);
|
||||
|
||||
return (
|
||||
<PluginContext.Provider value={this.plugin}>
|
||||
<DiffView
|
||||
oldText={oldText}
|
||||
newText={newText}
|
||||
onOldTextChange={setOldText}
|
||||
onNewTextChange={setNewText}
|
||||
/>
|
||||
</PluginContext.Provider>
|
||||
);
|
||||
};
|
||||
root.render(<App />);
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue