Merge pull request #891 from SeleiXi/feat/conflict-diff-jump

feat: add diff navigation to conflict resolver
This commit is contained in:
vorotamoroz
2026-05-25 17:51:49 +09:00
committed by GitHub
2 changed files with 78 additions and 23 deletions
@@ -27,6 +27,9 @@ export class ConflictResolveModal extends Modal {
localName: string = "Base"; localName: string = "Base";
remoteName: string = "Conflicted"; remoteName: string = "Conflicted";
offEvent?: ReturnType<typeof eventHub.onEvent>; offEvent?: ReturnType<typeof eventHub.onEvent>;
currentDiffIndex = -1;
diffView!: HTMLDivElement;
diffNavIndicator!: HTMLSpanElement;
constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) { constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string) {
super(app); super(app);
@@ -47,7 +50,7 @@ export class ConflictResolveModal extends Modal {
const lines = text.split("\n"); const lines = text.split("\n");
lines.forEach((line, index) => { lines.forEach((line, index) => {
const span = container.createSpan({ cls }); const span = container.createSpan({ cls });
span.textContent = line; span.setText(line);
if (index < lines.length - 1) { if (index < lines.length - 1) {
container.createSpan({ cls: "ls-mark-cr" }); container.createSpan({ cls: "ls-mark-cr" });
container.createEl("br"); container.createEl("br");
@@ -62,6 +65,33 @@ export class ConflictResolveModal extends Modal {
container.createEl("br"); container.createEl("br");
} }
navigateDiff(direction: "prev" | "next") {
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
if (diffElements.length === 0) return;
const prevFocused = this.diffView.querySelector(".diff-focused");
if (prevFocused) {
prevFocused.classList.remove("diff-focused");
}
if (direction === "next") {
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
} else {
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
}
const target = diffElements[this.currentDiffIndex];
target.classList.add("diff-focused");
target.scrollIntoView({ behavior: "smooth", block: "center" });
this.diffNavIndicator.setText(`${this.currentDiffIndex + 1}/${diffElements.length}`);
}
resetDiffNavigation() {
this.currentDiffIndex = -1;
const diffElements = this.diffView.querySelectorAll(".added, .deleted");
this.diffNavIndicator.setText(diffElements.length > 0 ? `0/${diffElements.length}` : "\u2014");
}
override onOpen() { override onOpen() {
const { contentEl } = this; const { contentEl } = this;
// Send cancel signal for the previous merge dialogue // Send cancel signal for the previous merge dialogue
@@ -78,10 +108,26 @@ export class ConflictResolveModal extends Modal {
// sendValue("close-resolve-conflict:" + this.filename, false); // sendValue("close-resolve-conflict:" + this.filename, false);
this.titleEl.setText(this.title); this.titleEl.setText(this.title);
contentEl.empty(); contentEl.empty();
contentEl.createEl("span", { text: this.filename }); const diffOptionsRow = contentEl.createDiv("");
const div = contentEl.createDiv(""); diffOptionsRow.addClass("diff-options-row");
div.addClass("op-scrollable"); diffOptionsRow.createEl("span", { text: this.filename });
div.addClass("ls-dialog");
const diffNavContainer = diffOptionsRow.createDiv("");
diffNavContainer.addClass("diff-nav");
diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => this.navigateDiff("prev"));
});
diffNavContainer.createEl("button", { text: "\u25BC Next" }, (e) => {
e.addClass("diff-nav-btn");
e.addEventListener("click", () => this.navigateDiff("next"));
});
this.diffNavIndicator = diffNavContainer.createEl("span", { text: "\u2014" });
this.diffNavIndicator.addClass("diff-nav-indicator");
this.diffView = contentEl.createDiv("");
this.diffView.addClass("op-scrollable");
this.diffView.addClass("ls-dialog");
let diffLength = 0; let diffLength = 0;
for (const v of this.result.diff) { for (const v of this.result.diff) {
const x1 = v[0]; const x1 = v[0];
@@ -91,12 +137,11 @@ export class ConflictResolveModal extends Modal {
continue; continue;
} }
if (x1 == DIFF_DELETE) { if (x1 == DIFF_DELETE) {
this.appendDiffFragment(div, x2, "deleted"); this.appendDiffFragment(this.diffView, x2, "deleted");
div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" });
} else if (x1 == DIFF_EQUAL) { } else if (x1 == DIFF_EQUAL) {
this.appendDiffFragment(div, x2, "normal"); this.appendDiffFragment(this.diffView, x2, "normal");
} else if (x1 == DIFF_INSERT) { } else if (x1 == DIFF_INSERT) {
this.appendDiffFragment(div, x2, "added"); this.appendDiffFragment(this.diffView, x2, "added");
} }
} }
@@ -108,24 +153,30 @@ export class ConflictResolveModal extends Modal {
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : ""); new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
this.appendVersionInfo(div2, "deleted", this.localName, date1); this.appendVersionInfo(div2, "deleted", this.localName, date1);
this.appendVersionInfo(div2, "added", this.remoteName, date2); this.appendVersionInfo(div2, "added", this.remoteName, date2);
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) => contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) => {
e.addEventListener("click", () => this.sendResponse(this.result.right.rev)) e.addClass("conflict-action-button");
).style.marginRight = "4px"; e.addEventListener("click", () => this.sendResponse(this.result.right.rev));
contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) => });
e.addEventListener("click", () => this.sendResponse(this.result.left.rev)) contentEl.createEl("button", { text: `Use ${this.remoteName}` }, (e) => {
).style.marginRight = "4px"; e.addClass("conflict-action-button");
e.addEventListener("click", () => this.sendResponse(this.result.left.rev));
});
if (!this.pluginPickMode) { if (!this.pluginPickMode) {
contentEl.createEl("button", { text: "Concat both" }, (e) => contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)) e.addClass("conflict-action-button");
).style.marginRight = "4px"; e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT));
});
} }
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) => {
e.addEventListener("click", () => this.sendResponse(CANCELLED)) e.addClass("conflict-action-button");
).style.marginRight = "4px"; e.addEventListener("click", () => this.sendResponse(CANCELLED));
});
if (diffLength > 100 * 1024) { if (diffLength > 100 * 1024) {
div.empty(); this.diffView.empty();
div.innerText = "(Too large diff to display)"; this.diffView.setText("(Too large diff to display)");
} }
this.resetDiffNavigation();
this.navigateDiff("next");
} }
sendResponse(result: MergeDialogResult) { sendResponse(result: MergeDialogResult) {
+4
View File
@@ -17,6 +17,10 @@
min-width: 5em; min-width: 5em;
} }
.conflict-action-button {
margin-right: 4px;
}
.op-scrollable { .op-scrollable {
overflow-y: scroll; overflow-y: scroll;
/* min-height: 280px; */ /* min-height: 280px; */