Files
ElyOffice/apps/ely-tree/src/conflict_pane.rs
T
ZacharyZhang-NY d59159cea2 Fixes for M-02 to M-14, G-01 to G-14, K-02 to K-04, X-04 to X-14, not yet reviewed
Committed before review so Windows and WSL builds can start.
Each fix was tested and mutation-checked here; the codex review
of each item is still to come, so tasks/BUGS.md stays unticked.

M-02 is mid-review: round 1 findings are fixed (apps/ely-mark/src/tally.rs).
K-01 stays deferred. The ElyMark window test mark-outside.sh failed
once at its crash-recovery step in the last run; not yet looked into.
2026-10-10 02:29:19 -04:00

231 lines
7.8 KiB
Rust

//! A conflict's middle, and the running operation's alert.
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
feedback::{Alert, EmptyState},
git::ThreeWayMerge,
primitives::{IconName, Severity},
theme::{ActiveTheme as _, ControlSize},
};
use gpui::{
AnyElement, App, Context, IntoElement, ParentElement as _, SharedString, Styled as _, Window,
div,
};
use crate::{
conflict::Stages,
ops::{Doing, Op},
tree::{Tree, text},
};
type Act = Box<dyn Fn(&mut Tree, &mut Context<Tree>)>;
/// Words for ours and theirs, as this operation means.
pub fn sides(doing: Option<&Doing>, cx: &App) -> [SharedString; 2] {
match doing {
Some(doing) => {
let (ours, theirs) = match doing.op {
Op::Merge => ("tree.ours.merge", "tree.theirs.merge"),
Op::Rebase => ("tree.ours.rebase", "tree.theirs.rebase"),
Op::CherryPick => ("tree.ours.pick", "tree.theirs.pick"),
Op::Revert => ("tree.ours.revert", "tree.theirs.revert"),
Op::Am => ("tree.ours.am", "tree.theirs.am"),
};
[
text(cx, ours, &[("name", &doing.ours)]),
text(cx, theirs, &[("name", &doing.theirs)]),
]
}
None => [text(cx, "tree.ours", &[]), text(cx, "tree.theirs", &[])],
}
}
fn button(
id: &str,
key: &str,
variant: ButtonVariant,
act: Act,
cx: &mut Context<Tree>,
) -> AnyElement {
let this = cx.weak_entity();
Button::new(SharedString::from(id.to_string()), text(cx, key, &[]))
.size(ControlSize::Sm)
.variant(variant)
.on_click(move |_, _: &mut Window, cx: &mut App| {
this.update(cx, |tree, cx| act(tree, cx)).ok();
})
.into_any_element()
}
impl Tree {
/// A stopped operation: continue, skip or abort.
pub fn op_alert(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let doing = match self.active().map(|opened| opened.doing.as_ref()) {
Some(Some(Ok(Some(doing)))) => doing,
// A read that failed says so in Git's own words.
Some(Some(Err(error))) => {
let title = text(cx, "tree.op.unread", &[("error", error)]);
return Some(Alert::new("tree-op", Severity::Danger, title).into_any_element());
}
_ => return None,
};
let op = doing.op;
let key = match op {
Op::Merge => "tree.op.merging",
Op::Rebase => "tree.op.rebasing",
Op::CherryPick => "tree.op.picking",
Op::Revert => "tree.op.reverting",
Op::Am => "tree.op.applying",
};
let title = text(cx, key, &[("ours", &doing.ours), ("theirs", &doing.theirs)]);
let mut alert = Alert::new("tree-op", Severity::Warning, title)
.body(text(cx, "tree.op.next", &[]))
.action(button(
"tree-op-continue",
"tree.op.continue",
ButtonVariant::Primary,
Box::new(move |tree, cx| tree.op_continue(op, cx)),
cx,
));
if op.skips() {
alert = alert.action(button(
"tree-op-skip",
"tree.op.skip",
ButtonVariant::Secondary,
Box::new(move |tree, cx| tree.op_skip(op, cx)),
cx,
));
}
Some(
alert
.action(button(
"tree-op-abort",
"tree.op.abort",
ButtonVariant::Danger,
Box::new(move |tree, cx| tree.op_abort(op, cx)),
cx,
))
.into_any_element(),
)
}
pub fn conflict_pane(&self, path: &str, stages: &Stages, cx: &mut Context<Self>) -> AnyElement {
let colors = cx.theme().colors.clone();
let doing = self.active().and_then(|opened| match &opened.doing {
Some(Ok(doing)) => doing.clone(),
_ => None,
});
let [ours, theirs] = sides(doing.as_ref(), cx);
let (text_view, present) = match stages {
Stages::Text { .. } => (self.merging.as_ref(), [true; 3]),
Stages::Whole { present } => (None, *present),
};
let mut actions: Vec<(&str, &str, ButtonVariant, Act)> = Vec::new();
if text_view.is_some() {
actions.push((
"tree-merge-save",
"tree.conflict.save",
ButtonVariant::Secondary,
Box::new(|tree, cx| tree.save_merge(cx)),
));
}
actions.push((
"tree-merge-done",
"tree.conflict.resolved",
ButtonVariant::Primary,
Box::new(|tree, cx| tree.mark_resolved(cx)),
));
// A side's whole file only where that side has one.
if present[1] {
actions.push((
"tree-merge-ours",
"tree.conflict.use_ours",
ButtonVariant::Ghost,
Box::new(|tree, cx| tree.take_side(true, cx)),
));
}
if present[2] {
actions.push((
"tree-merge-theirs",
"tree.conflict.use_theirs",
ButtonVariant::Ghost,
Box::new(|tree, cx| tree.take_side(false, cx)),
));
}
if !present[1] || !present[2] {
actions.push((
"tree-merge-delete",
"tree.conflict.delete",
ButtonVariant::Danger,
Box::new(|tree, cx| tree.resolve_deleted(cx)),
));
}
let buttons: Vec<AnyElement> = actions
.into_iter()
.map(|(id, key, variant, act)| button(id, key, variant, act, cx))
.collect();
let head = div()
.flex()
.items_center()
.gap_2()
.px_2()
.py_1p5()
.border_b_1()
.border_color(colors.border)
.child(
div()
.flex_1()
.min_w_0()
.text_color(colors.fg)
.child(path.to_string()),
)
.children(buttons);
let Some(merging) = text_view else {
let key = match present {
[_, false, _] => "tree.conflict.gone_ours",
[_, _, false] => "tree.conflict.gone_theirs",
_ => "tree.conflict.whole",
};
let body = EmptyState::new(
"tree-conflict-whole",
IconName::TriangleAlert,
text(cx, key, &[]),
)
.body(format!("{ours} · {theirs}"));
return div()
.size_full()
.flex()
.flex_col()
.child(head)
.child(body)
.into_any_element();
};
let base = text(cx, "tree.base", &[]);
let this = cx.weak_entity();
let merge = ThreeWayMerge::new(
SharedString::from(format!("tree-merge-{path}")),
[ours.as_ref(), base.as_ref(), theirs.as_ref()],
merging.regions.clone(),
)
.takes(merging.takes.clone())
.on_take(move |at, take, _, cx| {
this.update(cx, |tree, cx| tree.take(at, take, cx)).ok();
});
div()
.size_full()
.flex()
.flex_col()
.child(head)
.child(div().flex_1().min_h_0().p_2().child(merge))
.child(
div()
.flex_1()
.min_h_0()
.border_t_1()
.border_color(colors.border)
.child(self.merge_editor.clone()),
)
.into_any_element()
}
}