use gpui::prelude::*; use gpui::{AnyElement, Context, Entity, SharedString, WeakEntity, Window, div, px}; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::input::{Editor, EditorState}; use gpui_component::list::ListItem; use gpui_component::spinner::Spinner; use gpui_component::text::{TextView, TextViewState}; use gpui_component::tree::{TreeEntry, TreeState, tree}; use gpui_component::{ActiveTheme, Sizable, StyledExt, h_flex, v_flex}; use signed_ui::{placeholder, tree_row}; use super::RepoDetailView; use super::helpers::{code_language, is_markdown_path}; /// Width of the file explorer column. const TREE_WIDTH: f32 = 240.; /// Files larger than this are not previewed. pub(super) const MAX_PREVIEW_BYTES: usize = 1024 * 1024; /// Preview cache caps: at most this many files (or this many text bytes) /// are kept in memory at once; the oldest previews are evicted beyond that. pub(super) const MAX_PREVIEWED_FILES: usize = 32; pub(super) const MAX_PREVIEW_CACHE_BYTES: usize = 8 * 1024 * 1024; /// Preview state of a browsed file. pub(super) enum FileContent { /// Decodable text content. Text(String), /// Not valid UTF-8. Binary, /// Bigger than [`MAX_PREVIEW_BYTES`]. TooLarge, /// Reading failed. Failed(String), } /// A markdown document loaded into a persistent [`TextViewState`]. /// /// The state is owned by the view rather than created per render: GPUI /// drops keyed element state after one absent frame, which would re-parse /// the whole document on every pane switch (README / file / spinner). pub(super) struct MarkdownView { /// Source path; `None` means the repository README. pub(super) path: Option, pub(super) state: Entity, } /// A code file loaded into a persistent [`InputState`], rendered as a /// disabled (read-only) code editor with syntax highlighting, line numbers /// and search. Persistent for the same reason as [`MarkdownView`]. pub(super) struct CodeView { /// Source path, relative to the worktree root. pub(super) path: SharedString, pub(super) state: Entity, } /// Spinner shown while a document is being loaded/parsed. fn preview_spinner() -> AnyElement { v_flex() .size_full() .items_center() .justify_center() .child(Spinner::new().small()) .into_any_element() } impl RepoDetailView { /// One row of the file tree: icon + name, indented by depth. fn render_tree_item( ix: usize, entry: &TreeEntry, selected: bool, view: &WeakEntity, ) -> ListItem { let view = view.clone(); let id = entry.item().id.clone(); tree_row(ix, entry, selected, move |window, cx| { if let Some(view) = view.upgrade() { view.update(cx, |this, cx| this.open_file(&id, window, cx)); } }) } /// Left column: the file tree. pub(super) fn render_tree_column( tree_state: Entity, view: WeakEntity, cx: &mut Context, ) -> impl IntoElement { v_flex() .h_full() .w(px(TREE_WIDTH)) .p_2() .flex_none() .border_r_1() .border_color(cx.theme().border) .child(div().flex_1().min_h_0().child(tree( &tree_state, move |ix, entry, selected, _window, _cx| { Self::render_tree_item(ix, entry, selected, &view) }, ))) } /// Right column: README, selected file preview, or status text. pub(super) fn render_content_column( &self, pane_title: SharedString, cx: &mut Context, ) -> impl IntoElement { let loading = self.loading; let error = self.error.clone(); let selected_file = self.selected_file.clone(); let body: AnyElement = if loading { v_flex() .size_full() .items_center() .justify_center() .gap_2() .child(Spinner::new().small()) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child("Cloning repository..."), ) .into_any_element() } else if let Some(error) = error { v_flex() .size_full() .items_center() .justify_center() .p_4() .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child(error), ) .into_any_element() } else if let Some(path) = selected_file { match self.files.get(path.as_ref()) { Some(FileContent::Text(_)) => { if is_markdown_path(path.as_ref()) { self.markdown_element(Some(path.as_ref()), cx) } else { self.code_element(path.as_ref(), cx) } } Some(FileContent::Binary) => placeholder("Binary file — preview not supported", cx), Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx), Some(FileContent::Failed(message)) => placeholder(message, cx), None => preview_spinner(), } } else if self.readme_name.is_some() { self.markdown_element(None, cx) } else { placeholder("No README found", cx) }; // Latest commit for the current pane: the selected file, or the README // while nothing is selected. Computed after the body above, which // needs `&mut self`. let commit = match &self.selected_file { Some(path) => self.commits.get(path.as_ref()), None => self .readme_name .as_ref() .and_then(|name| self.commits.get(name.as_ref())), }; v_flex() .flex_1() .min_w_0() .h_full() .child( h_flex() .px_3() .h_9() .gap_2() .bg(cx.theme().muted) .border_b(px(1.)) .border_color(cx.theme().border) .text_color(cx.theme().muted_foreground) .child( div() .text_xs() .font_semibold() .text_ellipsis() .whitespace_nowrap() .child(pane_title), ) .when_some(commit, |this, commit| { this.child( h_flex() .flex_1() .gap_1() .child( Button::new("commit") .xsmall() .text() .label(commit.id.clone()), ) .child( div() .max_w(px(250.)) .text_xs() .text_ellipsis() .whitespace_nowrap() .child(commit.summary.clone()), ), ) }), ) .child(div().id("repo-content").flex_1().min_h_0().child(body)) } /// Load `text` into the persistent markdown TextView state. /// /// The state is created empty and fed via `push_str`, which parses on a /// background task, so switching files never blocks the main thread. pub(super) fn set_markdown( &mut self, path: Option, text: &str, cx: &mut Context, ) { let state = cx.new(|cx| TextViewState::markdown("", cx)); state.update(cx, |state, cx| state.push_str(text, cx)); self.md = Some(MarkdownView { path, state }); } /// The persistent markdown TextView for `path` (`None` = README), or a /// spinner while the document is being loaded/parsed. fn markdown_element(&self, path: Option<&str>, _cx: &mut Context) -> AnyElement { let Some(md) = &self.md else { return preview_spinner(); }; let ready = match path { Some(path) => md.path.as_deref() == Some(path), None => md.path.is_none(), }; if !ready { return preview_spinner(); } TextView::new(&md.state) .selectable(true) .scrollable(true) .p_4() .text_sm() .into_any_element() } /// Load `text` into the persistent code editor state for `path`. /// /// The state is created in code editor mode so the Input renders it as /// a syntax-highlighted, read-only editor; the tree-sitter parse runs /// on a background task like [`set_markdown`]'s. pub(super) fn set_code( &mut self, path: SharedString, text: &str, window: &mut Window, cx: &mut Context, ) { let language = code_language(path.as_ref()).unwrap_or("text"); let state = cx.new(|cx| { EditorState::new(window, cx) .language(language) .default_value(text) .line_number(true) .folding(true) }); self.code = Some(CodeView { path, state }); } /// The persistent code editor for `path`, or a spinner while the file is /// being loaded/parsed. fn code_element(&self, path: &str, _cx: &mut Context) -> AnyElement { let Some(code) = &self.code else { return preview_spinner(); }; if code.path.as_ref() != path { return preview_spinner(); } Editor::new(&code.state) .readonly(true) .bordered(false) .rounded_none() .h_full() .text_sm() .into_any_element() } }