use gpui::{ AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, img, prelude::FluentBuilder, }; use crate::{ api::{self}, app, component::{ button::{self, Button, button}, font_icon::{FontIcon, font_icon}, markdown::{self, MarkdownText}, text::text, }, query::{self, QueryStatus, read_query, use_query}, }; pub(crate) struct PullRequestView { markdown_viewer: Option>, pull_request_query: Option>, } #[derive(gpui::IntoElement)] struct Toolbar {} pub fn new(_cx: &mut gpui::Context) -> PullRequestView { PullRequestView { markdown_viewer: None, pull_request_query: None, } } impl PullRequestView { pub(crate) fn change_displayed_pull_request( &mut self, id: api::issues::Id, cx: &mut gpui::Context, ) { let query = use_query(api::issues::FetchPullRequest { id }, cx); self.pull_request_query = Some(query.clone()); _ = cx .observe(&query.clone(), move |this, _, cx| { this.load_markdown_content(cx); }) .detach(); // cached query will not trigger observe callback // this is required so that content is loaded immediately for cached query self.load_markdown_content(cx); cx.notify(); } fn load_markdown_content(&mut self, cx: &mut gpui::Context) { let Some(query) = &self.pull_request_query else { return; }; let maybe_content = { let data = read_query(&query, cx); if let QueryStatus::Loaded(pr) = data { Some(gpui::SharedString::new(pr.body.as_str())) } else { None } }; self.markdown_viewer = maybe_content.map(|content| cx.new(|cx| markdown::new(content, cx))); cx.notify(); } fn pr_content( &self, pr: &api::issues::DetailedPullRequest, cx: &gpui::Context, ) -> gpui::AnyElement { let theme = app::current_theme(cx); let mut status_pill = div() .flex() .flex_row() .items_center() .gap_1() .px_2() .rounded_full(); match pr.state { | api::issues::PullRequestState::Open => { status_pill = status_pill .bg(theme.colors.success_solid) .child( font_icon(FontIcon::PullRequestArrow) .size_3() .text_color(theme.colors.success_on_solid), ) .child( text("Open") .text_color(theme.colors.success_on_solid) .text_xs(), ); } | api::issues::PullRequestState::Closed => { status_pill = status_pill .bg(theme.colors.danger_solid) .child( font_icon(FontIcon::PullRequestClosed) .size_3() .text_color(theme.colors.danger_on_solid), ) .child( text("Closed") .text_color(theme.colors.danger_on_solid) .text_xs(), ); } | api::issues::PullRequestState::Merged => { status_pill = status_pill.bg(theme.colors.accent_solid).child( text("Merged") .text_color(theme.colors.accent_on_solid) .text_xs(), ); } } let merge_text = match ( pr.author.as_ref(), pr.base_branch_name.as_ref(), pr.head_branch_name.as_ref(), ) { | (Some(author), Some(base_branch), Some(head_branch)) => { let str = format!( "{} requested to merge {} into {}", author.login, head_branch, base_branch ); let head_branch_text_offset = author.login.len() + 20; let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6; let highlights = [ ( 0..author.login.len(), gpui::HighlightStyle { font_weight: Some(gpui::FontWeight::BOLD), ..Default::default() }, ), ( head_branch_text_offset..head_branch_text_offset + head_branch.len(), gpui::HighlightStyle { font_weight: Some(gpui::FontWeight::BOLD), color: Some(theme.colors.accent_fg.into()), ..Default::default() }, ), ( base_branch_text_offset..base_branch_text_offset + base_branch.len(), gpui::HighlightStyle { font_weight: Some(gpui::FontWeight::BOLD), color: Some(theme.colors.accent_fg.into()), ..Default::default() }, ), ]; Some(( author, gpui::StyledText::new(str).with_highlights(highlights), )) } | _ => None, }; let metadata_line = div() .flex() .flex_row() .gap_2() .when_some(merge_text, |it, (author, t)| { it.child( div() .flex() .flex_row() .items_center() .gap_1p5() .child(img(author.avatar_url.clone()).size_4().rounded_full()) .child( div() .min_w_0() .w_full() .text_color(theme.colors.text) .text_xs() .font_weight(gpui::FontWeight::LIGHT) .opacity(0.8) .child(t), ), ) }); div() .size_full() .flex() .flex_col() .overflow_hidden() .child(Toolbar {}) .child( div() .flex() .flex_row() .justify_between() .items_center() .w_full() .px_3p5() .py_3() .border_b_1() .border_color(theme.colors.border) .child( div() .w_full() .flex() .flex_col() .items_start() .child(text(pr.title.clone()).w_full().text_xl().mb_1()) .child(metadata_line), ) .child(div().flex().flex_col().items_end().gap_1().when_some( pr.created_at, |it, created_at| { it.child( text(created_at.format("%Y-%m-%d %H:%M").to_string()) .opacity(0.5) .text_xs(), ) .child(status_pill) }, )), ) .child( div().flex_1().min_h_0().w_full().child( div() .id("pr-body-content") .size_full() .overflow_y_scroll() .when_some(self.markdown_viewer.as_ref(), |it, viewer| { it.child(div().w_full().p_3p5().child(viewer.clone())) }), ), ) .into_any_element() } } impl gpui::Render for PullRequestView { fn render( &mut self, _window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { div().size_full().child(match &self.pull_request_query { | Some(q) => match read_query(q, cx) { | QueryStatus::Loaded(pr) => self.pr_content(pr, cx), | QueryStatus::Err(e) => div() .size_full() .child(format!("{:?}", e)) .into_any_element(), | QueryStatus::Loading => div() .size_full() .child("loading pr content") .into_any_element(), }, | None => div().size_full().child("no pr selected").into_any_element(), }) } } impl gpui::RenderOnce for Toolbar { fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { fn toolbar_button(id: impl Into) -> Button { button(id) .px_2p5() .py_1() .variant(button::Variant::Secondary) } fn divider() -> gpui::Div { div().h_full().w_px() } let theme = app::current_theme(cx); div() .w_full() .flex() .flex_row() .items_center() .justify_start() .p_1() .bg(theme.colors.background) .border_b_1() .border_color(theme.colors.border) .child( toolbar_button("pr-review-btn") .leading(font_icon(FontIcon::Eye)) .mr_1(), ) .child( toolbar_button("pr-review-btn") .leading(font_icon(FontIcon::RefreshCw)) .mr_2(), ) .child(divider().bg(theme.colors.border).mr_2()) .child(toolbar_button("pr-review-btn").leading(font_icon(FontIcon::Star))) .child(div().flex_1()) .child( toolbar_button("pr-close-btn") .leading(font_icon(FontIcon::PullRequestClosed)) .mr_1(), ) .child( toolbar_button("pr-merge-btn") .variant(button::Variant::Primary) .leading(font_icon(FontIcon::GitMerge)) .rounded_r_none(), ) .child(divider()) .child( toolbar_button("chevron") .py_1() .px_0p5() .variant(button::Variant::Primary) .leading(font_icon(FontIcon::ChevronDown)) .rounded_l_none(), ) } }