дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Blocks that were moved unchanged, re-indented or moved and then edited are paired across the split diff: each side gets a link to the other (keyboard accessible), moved lines carry a left accent bar, edits inside a moved block get word-level highlights, and the toolbar counts moved blocks and whitespace-only changes. Staging and reverting still work on the original hunks. Co-authored-by: shohruh-genius <[email protected]>
118 рядки
4.3 KiB
TypeScript
118 рядки
4.3 KiB
TypeScript
/** Moved-code highlights and keyboard-accessible links between split diff panes. */
|
|
import { Decoration, EditorView, WidgetType, type DecorationSet } from "@codemirror/view";
|
|
import { getChunks, mergeViewSiblings, uncollapseUnchanged } from "@codemirror/merge";
|
|
import { tx } from "@/platform/i18n";
|
|
import { chunkDecorations } from "./cm";
|
|
import { getDiffInsights } from "./diffOptions";
|
|
|
|
const movedLine = Decoration.line({ class: "cm-diffMoved" });
|
|
|
|
class MovedCodeLink extends WidgetType {
|
|
constructor(
|
|
readonly target: number,
|
|
readonly line: number,
|
|
readonly edited: boolean,
|
|
) {
|
|
super();
|
|
}
|
|
eq(other: MovedCodeLink) {
|
|
return other.target === this.target && other.line === this.line && other.edited === this.edited;
|
|
}
|
|
toDOM(view: EditorView) {
|
|
const button = document.createElement("button");
|
|
const isA = getChunks(view.state)?.side === "a";
|
|
button.type = "button";
|
|
button.className = "cm-diffMoveLink";
|
|
if (this.edited)
|
|
button.title = isA
|
|
? tx`Moved to line ${this.line} with internal edits`
|
|
: tx`Moved from line ${this.line} with internal edits`;
|
|
button.textContent = this.edited
|
|
? isA
|
|
? tx`To L${this.line} (edited)`
|
|
: tx`From L${this.line} (edited)`
|
|
: isA
|
|
? tx`Moved to line ${this.line}`
|
|
: tx`Moved from line ${this.line}`;
|
|
button.setAttribute(
|
|
"aria-label",
|
|
isA ? tx`Go to moved code at line ${this.line}` : tx`Go to original code at line ${this.line}`,
|
|
);
|
|
button.onclick = () => {
|
|
const panes = mergeViewSiblings(view);
|
|
if (!panes) return;
|
|
const other = isA ? panes.b : panes.a;
|
|
if (!other) return;
|
|
other.dispatch({
|
|
selection: { anchor: this.target },
|
|
effects: [uncollapseUnchanged.of(this.target), EditorView.scrollIntoView(this.target, { y: "center" })],
|
|
});
|
|
other.focus();
|
|
};
|
|
return button;
|
|
}
|
|
ignoreEvent() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function decorations(view: EditorView, words: boolean): DecorationSet {
|
|
const info = getChunks(view.state);
|
|
if (!info?.side) return Decoration.none;
|
|
const isA = info.side === "a";
|
|
const ranges = getDiffInsights(info.chunks).moves.flatMap((m) => {
|
|
const [from, to, target] = isA ? [m.fromA, m.toA, m.fromB] : [m.fromB, m.toB, m.fromA];
|
|
const line = isA ? m.lineB : m.lineA;
|
|
const edits = words
|
|
? m.changes.flatMap((c) => {
|
|
const start = isA ? c.start1 : c.start2;
|
|
const end = isA ? c.end1 : c.end2;
|
|
return start === end
|
|
? []
|
|
: [
|
|
Decoration.mark({
|
|
class: `cm-diffMoveEdit cm-diffMoveEdit-${isA ? "deleted" : "inserted"}`,
|
|
attributes: { title: tx("Edited within moved code") },
|
|
}).range(from + start, from + end),
|
|
];
|
|
})
|
|
: [];
|
|
// Whole lines, not the text: the chunk's own background already shows the change.
|
|
const lines = [];
|
|
for (let line = view.state.doc.lineAt(from); ; line = view.state.doc.line(line.number + 1)) {
|
|
lines.push(movedLine.range(line.from));
|
|
if (line.to >= to - 1 || line.number === view.state.doc.lines) break;
|
|
}
|
|
return [
|
|
...lines,
|
|
...edits,
|
|
Decoration.widget({ widget: new MovedCodeLink(target, line, m.changes.length > 0), side: 1 }).range(
|
|
view.state.doc.lineAt(from).to,
|
|
),
|
|
];
|
|
});
|
|
return Decoration.set(ranges, true);
|
|
}
|
|
|
|
export const movedCodeHighlights = (words: boolean) => [
|
|
chunkDecorations((view) => decorations(view, words)),
|
|
EditorView.baseTheme({
|
|
".cm-diffMoved": { boxShadow: "inset 3px 0 0 color-mix(in srgb, var(--accent) 70%, transparent)" },
|
|
".cm-diffMoveEdit-deleted": { backgroundColor: "color-mix(in srgb, var(--diff-deleted) 85%, transparent)" },
|
|
".cm-diffMoveEdit-inserted": { backgroundColor: "color-mix(in srgb, var(--diff-inserted) 85%, transparent)" },
|
|
".cm-diffMoveLink": {
|
|
marginLeft: "12px",
|
|
padding: "0 4px",
|
|
fontFamily: "var(--font-ui)",
|
|
fontSize: "var(--fs-small)",
|
|
color: "var(--link)",
|
|
background: "var(--bg-app)",
|
|
border: "1px solid var(--border)",
|
|
borderRadius: "var(--radius-small)",
|
|
cursor: "pointer",
|
|
},
|
|
".cm-diffMoveLink:hover": { background: "var(--bg-hover)" },
|
|
".cm-diffMoveLink:focus-visible": { outline: "2px solid var(--accent)", outlineOffset: "2px" },
|
|
}),
|
|
];
|