feat: pull request and patch #13

Merged
reya merged 9 commits from feat/improve-ui into master 2026-09-02 10:49:31 +00:00
Showing only changes of commit c03ce50c82 - Show all commits
@@ -15,7 +15,7 @@ use gpui::{
Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, relative,
size, size,
}; };
use gpui_base::Button as BaseButton; use gpui_base::{Button as BaseButton, StyledExt};
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::combobox::{ use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext, Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
@@ -97,10 +97,9 @@ impl NewPullRequestView {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
let description = cx
.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change (optional)"));
let pane = cx.new(DiffPane::new); let pane = cx.new(DiffPane::new);
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
let description = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe..."));
let base_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| { let base_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| {
ComboboxState::new( ComboboxState::new(
@@ -111,6 +110,7 @@ impl NewPullRequestView {
) )
.searchable(true) .searchable(true)
}); });
let compare_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| { let compare_select: Entity<ComboboxState<SearchableVec<SharedString>>> = cx.new(|cx| {
ComboboxState::new( ComboboxState::new(
SearchableVec::new(Vec::<SharedString>::new()), SearchableVec::new(Vec::<SharedString>::new()),
@@ -181,8 +181,6 @@ impl NewPullRequestView {
/// (defaults: the announced HEAD branch for the base, the checkout's /// (defaults: the announced HEAD branch for the base, the checkout's
/// current branch for the compare) and load the compare. /// current branch for the compare) and load the compare.
fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let handle = window.window_handle();
let prompt = cx.prompt_for_paths(PathPromptOptions { let prompt = cx.prompt_for_paths(PathPromptOptions {
files: false, files: false,
directories: true, directories: true,
@@ -190,32 +188,36 @@ impl NewPullRequestView {
prompt: Some("Choose local checkout".into()), prompt: Some("Choose local checkout".into()),
}); });
let task = cx.spawn(async move |this, cx| { let task = cx.spawn_in(window, async move |this, cx| {
if let Ok(Ok(Some(mut paths))) = prompt.await // `Ok(Ok(Some(paths)))` means the user picked a folder; a
&& let Some(path) = paths.pop() // cancel (or a picker failure) resolves to anything else.
{ let picked = match prompt.await {
let path = path.to_string_lossy().to_string(); Ok(Ok(Some(mut paths))) => paths.pop(),
// Branches and the current branch are read off the UI thread. _ => None,
let info = cx };
.background_executor() let Some(path) = picked else {
.spawn({ return Ok(());
let path = path.clone(); };
async move { let path = path.to_string_lossy().to_string();
let repo = gix::open(Path::new(&path)).ok()?;
let branches = // Branches and the current branch are read off the UI thread.
signed_git::worktree_branches(Path::new(&path)).unwrap_or_default(); let info = cx
let current = signed_git::current_branch(&repo).ok().flatten(); .background_spawn({
Some((branches, current)) let path = path.clone();
} async move {
}) let repo = gix::open(Path::new(&path)).ok()?;
.await; let branches =
signed_git::worktree_branches(Path::new(&path)).unwrap_or_default();
let current = signed_git::current_branch(&repo).ok().flatten();
Some((branches, current))
}
})
.await;
this.update_in(cx, |this, window, cx| {
this.apply_checkout(path, info, window, cx);
})?;
let _ = handle.update(cx, |_, window, cx| {
let _ = this.update(cx, |this, cx| {
this.apply_checkout(path, info, window, cx);
});
});
}
Ok(()) Ok(())
}); });
self.tasks.push(task); self.tasks.push(task);
@@ -496,54 +498,59 @@ impl NewPullRequestView {
h_flex() h_flex()
.px_4() .px_4()
.h_12() .h_16()
.w_full() .w_full()
.gap_2() .gap_2()
.items_center() .items_end()
.child( .child(
div() v_flex()
.text_xs() .gap_1()
.text_color(cx.theme().muted_foreground) .child(
.child("base"), div()
.text_xs()
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child("Merge Into"),
)
.child(
div().w(px(140.)).child(
Combobox::new(&self.base_select)
.placeholder("branch")
.appearance(false)
.menu_width(px(220.))
.disabled(!has_checkout)
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
}),
),
),
) )
.child( .child(
div().w(px(140.)).child( v_flex()
Combobox::new(&self.base_select) .gap_1()
.placeholder("branch") .child(
.appearance(false) div()
.menu_width(px(220.)) .text_xs()
.disabled(!has_checkout) .font_semibold()
.bg(cx.theme().muted) .text_color(cx.theme().muted_foreground)
.rounded(cx.theme().radius) .child("Pull From"),
.render_trigger(|ctx, _window, cx| { )
render_ref_trigger(ctx, CustomIconName::GitBranch, cx) .child(
}), div().w(px(140.)).child(
), Combobox::new(&self.compare_select)
) .placeholder("branch")
.child( .appearance(false)
Icon::new(IconName::ArrowRight) .menu_width(px(220.))
.small() .disabled(!has_checkout)
.text_color(cx.theme().muted_foreground), .bg(cx.theme().muted)
) .rounded(cx.theme().radius)
.child( .render_trigger(|ctx, _window, cx| {
div() render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
.text_xs() }),
.text_color(cx.theme().muted_foreground) ),
.child("compare"), ),
)
.child(
div().w(px(140.)).child(
Combobox::new(&self.compare_select)
.placeholder("branch")
.appearance(false)
.menu_width(px(220.))
.disabled(!has_checkout)
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.render_trigger(|ctx, _window, cx| {
render_ref_trigger(ctx, CustomIconName::GitBranch, cx)
}),
),
) )
.child( .child(
Button::new("choose-checkout") Button::new("choose-checkout")
@@ -561,7 +568,7 @@ impl NewPullRequestView {
.child( .child(
Button::new("create-pr") Button::new("create-pr")
.primary() .primary()
.label("Create pull request") .icon(IconName::Plus)
.loading(self.submitting) .loading(self.submitting)
.disabled(!can_submit) .disabled(!can_submit)
.on_click(cx.listener(|this, _event, window, cx| { .on_click(cx.listener(|this, _event, window, cx| {
@@ -575,11 +582,10 @@ impl NewPullRequestView {
fn render_inputs(&self, _cx: &mut Context<Self>) -> AnyElement { fn render_inputs(&self, _cx: &mut Context<Self>) -> AnyElement {
v_flex() v_flex()
.px_4() .px_4()
.py_2()
.w_full() .w_full()
.gap_2() .gap_2()
.child(Input::new(&self.subject)) .child(Input::new(&self.subject))
.child(Textarea::new(&self.description).h_32()) .child(Textarea::new(&self.description).h_24())
.into_any_element() .into_any_element()
} }
@@ -590,7 +596,7 @@ impl NewPullRequestView {
h_flex() h_flex()
.px_4() .px_4()
.h_9() .pb_4()
.w_full() .w_full()
.gap_2() .gap_2()
.items_center() .items_center()
@@ -834,20 +840,24 @@ impl Render for NewPullRequestView {
v_flex() v_flex()
.id("new-pr") .id("new-pr")
.size_full() .size_full()
.child(self.render_compare_bar(cx)) .child(
.child(self.render_inputs(cx)) v_flex()
.when_some(self.error.clone(), |this, error| { .gap_4()
this.child( .child(self.render_compare_bar(cx))
h_flex() .child(self.render_inputs(cx))
.px_4() .when_some(self.error.clone(), |this, error| {
.py_1() this.child(
.w_full() h_flex()
.text_xs() .px_4()
.text_color(cx.theme().danger) .py_1()
.child(error), .w_full()
) .text_xs()
}) .text_color(cx.theme().danger)
.child(self.render_tabs(cx)) .child(error),
)
})
.child(self.render_tabs(cx)),
)
.child( .child(
v_flex() v_flex()
.flex_1() .flex_1()