Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,341 @@
|
||||
//! The middle: the shown file's diff, picture or size (TREE-03).
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant, IconButton, SegmentedControl},
|
||||
data_display::BeforeAfter,
|
||||
feedback::{EmptyState, ErrorView},
|
||||
git::{ChangeSection, DiffLayout, DiffStat, DiffViewer, LineKind, Stretch},
|
||||
primitives::{IconName, Image},
|
||||
theme::{ActiveTheme as _, ControlSize},
|
||||
typography::format,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, Context, ElementId, IntoElement, ObjectFit, ParentElement as _, Styled as _, div,
|
||||
prelude::FluentBuilder as _,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
diff::Space,
|
||||
pick::Shown,
|
||||
shown::View,
|
||||
tree::{Tree, text},
|
||||
};
|
||||
|
||||
fn space_key(space: Space) -> &'static str {
|
||||
match space {
|
||||
Space::Exact => "exact",
|
||||
Space::Changes => "changes",
|
||||
Space::All => "all",
|
||||
}
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// The header: path, counts, whitespace, layout, picked lines.
|
||||
fn pane_head(&self, shown: &Shown, cx: &mut Context<Self>) -> AnyElement {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let this = cx.weak_entity();
|
||||
let entry_from = self.active().and_then(|opened| match &opened.status {
|
||||
Some(Ok(status)) => status
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.path == shown.path)?
|
||||
.from
|
||||
.clone(),
|
||||
_ => None,
|
||||
});
|
||||
let path = match (&entry_from, shown.section) {
|
||||
(Some(from), ChangeSection::Staged) => format!("{from} → {}", shown.path),
|
||||
_ => shown.path.clone(),
|
||||
};
|
||||
let stat = match &shown.view {
|
||||
Some(Ok(View::Text { stretches, .. })) => Some(count(stretches)),
|
||||
_ => None,
|
||||
};
|
||||
let text_view = matches!(shown.view, Some(Ok(View::Text { .. })));
|
||||
let space = SegmentedControl::new("tree-space", space_key(self.space))
|
||||
.size(ControlSize::Sm)
|
||||
.segment("exact", text(cx, "tree.space.exact", &[]), None)
|
||||
.segment("changes", text(cx, "tree.space.changes", &[]), None)
|
||||
.segment("all", text(cx, "tree.space.all", &[]), None)
|
||||
.on_change({
|
||||
let this = this.clone();
|
||||
move |key, _, cx| {
|
||||
let space = match key.as_ref() {
|
||||
"changes" => Space::Changes,
|
||||
"all" => Space::All,
|
||||
_ => Space::Exact,
|
||||
};
|
||||
this.update(cx, |tree, cx| tree.set_space(space, cx)).ok();
|
||||
}
|
||||
});
|
||||
let layout = SegmentedControl::new(
|
||||
"tree-layout",
|
||||
if self.layout == DiffLayout::Split {
|
||||
"split"
|
||||
} else {
|
||||
"unified"
|
||||
},
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.segment("unified", text(cx, "tree.layout.unified", &[]), None)
|
||||
.segment("split", text(cx, "tree.layout.split", &[]), None)
|
||||
.on_change({
|
||||
let this = this.clone();
|
||||
move |key, _, cx| {
|
||||
let layout = if key.as_ref() == "split" {
|
||||
DiffLayout::Split
|
||||
} else {
|
||||
DiffLayout::Unified
|
||||
};
|
||||
this.update(cx, |tree, cx| {
|
||||
tree.layout = layout;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
let key = if shown.section == ChangeSection::Staged {
|
||||
"tree.lines.unstage"
|
||||
} else {
|
||||
"tree.lines.stage"
|
||||
};
|
||||
let lines = (!shown.picked.is_empty() && self.partial(shown)).then(|| {
|
||||
let this = this.clone();
|
||||
let n = shown.picked.len().to_string();
|
||||
Button::new("tree-lines", text(cx, key, &[("n", &n)]))
|
||||
.variant(ButtonVariant::Primary)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, _, cx| {
|
||||
this.update(cx, |tree, cx| tree.stage_picked(cx)).ok();
|
||||
})
|
||||
});
|
||||
// History and blame need a file Git already tracks.
|
||||
let tracked = matches!(
|
||||
shown.section,
|
||||
ChangeSection::Staged | ChangeSection::Unstaged | ChangeSection::Conflicted
|
||||
);
|
||||
let looks = tracked.then(|| self.look_buttons(&shown.path, cx));
|
||||
let open = {
|
||||
let (this, path) = (this.clone(), shown.path.clone());
|
||||
IconButton::new("tree-open-file", IconName::ExternalLink)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(text(cx, "tree.open_file", &[]))
|
||||
.on_click(move |_, _, cx| {
|
||||
this.update(cx, |tree, cx| tree.open_file(&path, cx)).ok();
|
||||
})
|
||||
};
|
||||
let tools = self.tool_buttons(&shown.path, cx);
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child(div().flex_1().min_w_0().text_color(colors.fg).child(path))
|
||||
.children(stat.map(|(added, removed)| DiffStat::new(added, removed)))
|
||||
.children(lines)
|
||||
.when(text_view, |head| head.child(space).child(layout))
|
||||
.children(looks.into_iter().flatten())
|
||||
.children(tools)
|
||||
.child(open)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Hunks and lines stage only on an exact plain diff.
|
||||
fn partial(&self, shown: &Shown) -> bool {
|
||||
let staging = matches!(
|
||||
shown.section,
|
||||
ChangeSection::Staged | ChangeSection::Unstaged
|
||||
);
|
||||
let plain = matches!(&shown.view, Some(Ok(View::Text { diff, .. })) if diff.partial());
|
||||
staging && plain && self.space == Space::Exact
|
||||
}
|
||||
|
||||
/// Why hunks and lines do not stage here.
|
||||
fn whole_note(&self, shown: &Shown, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let staging = matches!(
|
||||
shown.section,
|
||||
ChangeSection::Staged | ChangeSection::Unstaged | ChangeSection::Untracked
|
||||
);
|
||||
if !matches!(shown.view, Some(Ok(View::Text { .. }))) || !staging || self.partial(shown) {
|
||||
return None;
|
||||
}
|
||||
let key = if self.space == Space::Exact {
|
||||
"tree.whole"
|
||||
} else {
|
||||
"tree.space.note"
|
||||
};
|
||||
let colors = cx.theme().colors.clone();
|
||||
Some(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(text(cx, key, &[]))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pane(&self, shown: &Shown, cx: &mut Context<Self>) -> AnyElement {
|
||||
if let Some(Ok(View::Conflict(stages))) = &shown.view {
|
||||
return self.conflict_pane(&shown.path, stages, cx);
|
||||
}
|
||||
let body = match &shown.view {
|
||||
Some(Ok(View::Text { stretches, .. })) if hunks(stretches) => {
|
||||
let this = cx.weak_entity();
|
||||
let (open, line, hunk) = (this.clone(), this.clone(), this);
|
||||
let mut viewer = DiffViewer::from_stretches(
|
||||
("tree-diff", shown.repo),
|
||||
shown.path.clone(),
|
||||
stretches.clone(),
|
||||
)
|
||||
.headless()
|
||||
.layout(self.layout)
|
||||
.open(shown.opened.clone())
|
||||
.on_open(move |stretch, _, cx| {
|
||||
open.update(cx, |tree, cx| {
|
||||
if let Some(shown) = &mut tree.shown {
|
||||
shown.opened.push(stretch);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
if self.partial(shown) {
|
||||
let key = if shown.section == ChangeSection::Staged {
|
||||
"tree.hunk.unstage"
|
||||
} else {
|
||||
"tree.hunk.stage"
|
||||
};
|
||||
viewer = viewer
|
||||
.selected(shown.picked.iter().copied())
|
||||
.on_line(move |place, shift, _, cx| {
|
||||
line.update(cx, |tree, cx| tree.pick_line(place, shift, cx))
|
||||
.ok();
|
||||
})
|
||||
.hunk_action(text(cx, key, &[]), move |stretch, _, cx| {
|
||||
hunk.update(cx, |tree, cx| tree.stage_hunk(stretch, cx))
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
viewer.into_any_element()
|
||||
}
|
||||
view => self.still(view.as_ref(), cx),
|
||||
};
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.pane_head(shown, cx))
|
||||
.children(self.whole_note(shown, cx))
|
||||
.child(div().flex_1().min_h_0().child(body))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// A diff with nothing to stage: commits and file history.
|
||||
pub fn view_only(
|
||||
&self,
|
||||
id: ElementId,
|
||||
path: &str,
|
||||
view: Option<&Result<View, String>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
match view {
|
||||
Some(Ok(View::Text { stretches, .. })) if hunks(stretches) => {
|
||||
DiffViewer::from_stretches(id, path.to_string(), stretches.clone())
|
||||
.headless()
|
||||
.layout(self.layout)
|
||||
.into_any_element()
|
||||
}
|
||||
view => self.still(view, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything but a text diff with lines to show.
|
||||
fn still(&self, view: Option<&Result<View, String>>, cx: &mut Context<Self>) -> AnyElement {
|
||||
match view {
|
||||
None => div().into_any_element(),
|
||||
Some(Err(error)) => ErrorView::new(
|
||||
"tree-diff-error",
|
||||
IconName::TriangleAlert,
|
||||
text(cx, "tree.diff.unread", &[]),
|
||||
)
|
||||
.detail(error.clone())
|
||||
.into_any_element(),
|
||||
Some(Ok(View::Text { .. })) => EmptyState::new(
|
||||
"tree-diff-none",
|
||||
IconName::Check,
|
||||
text(cx, "tree.diff.none", &[]),
|
||||
)
|
||||
.into_any_element(),
|
||||
Some(Ok(View::Picture { old, new })) => {
|
||||
let picture = |id: &'static str, image: &std::sync::Arc<gpui::Image>| {
|
||||
Image::new(id, image.clone())
|
||||
.fit(ObjectFit::Contain)
|
||||
.size_full()
|
||||
};
|
||||
let shown = match (old, new) {
|
||||
(Some(old), Some(new)) => BeforeAfter::new(
|
||||
"tree-picture",
|
||||
picture("tree-old", old),
|
||||
picture("tree-new", new),
|
||||
)
|
||||
.size_full()
|
||||
.into_any_element(),
|
||||
(Some(only), None) | (None, Some(only)) => {
|
||||
picture("tree-only", only).into_any_element()
|
||||
}
|
||||
(None, None) => div().into_any_element(),
|
||||
};
|
||||
div().size_full().p_4().child(shown).into_any_element()
|
||||
}
|
||||
Some(Ok(View::Binary { old, new })) => {
|
||||
let size = |bytes: &Option<u64>| {
|
||||
bytes.map_or_else(|| "—".to_string(), |bytes| format::file_size(bytes, false))
|
||||
};
|
||||
let sizes = format!("{} → {}", size(old), size(new));
|
||||
EmptyState::new("tree-binary", IconName::File, text(cx, "tree.binary", &[]))
|
||||
.body(sizes)
|
||||
.into_any_element()
|
||||
}
|
||||
Some(Ok(View::Folder)) => EmptyState::new(
|
||||
"tree-folder",
|
||||
IconName::Folder,
|
||||
text(cx, "tree.folder", &[]),
|
||||
)
|
||||
.into_any_element(),
|
||||
Some(Ok(View::Conflict(_))) => EmptyState::new(
|
||||
"tree-conflicted",
|
||||
IconName::TriangleAlert,
|
||||
text(cx, "tree.conflicted", &[]),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Git printed any hunk; a pure rename has none.
|
||||
fn hunks(stretches: &[Stretch]) -> bool {
|
||||
stretches
|
||||
.iter()
|
||||
.any(|stretch| matches!(stretch, Stretch::Hunk { .. }))
|
||||
}
|
||||
|
||||
/// Lines added and removed in the hunks.
|
||||
fn count(stretches: &[Stretch]) -> (usize, usize) {
|
||||
stretches
|
||||
.iter()
|
||||
.filter_map(|stretch| match stretch {
|
||||
Stretch::Hunk { lines, .. } => Some(lines),
|
||||
Stretch::Folded(_) => None,
|
||||
})
|
||||
.flatten()
|
||||
.fold((0, 0), |(added, removed), line| match line.kind {
|
||||
LineKind::Added => (added + 1, removed),
|
||||
LineKind::Removed => (added, removed + 1),
|
||||
LineKind::Same => (added, removed),
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user