Nate ButlerCheckpoint0ae34d91 year agoHistory
1512 lines · 47.6 KB · Rust
RawDownload
use editor::Editor;use git::repository::GitFileStatus;use gpui::*;use settings::Settings;use std::collections::{BTreeMap, HashMap};use std::ops::Range;use theme::ThemeSettings;use ui::{prelude::*, Checkbox, DividerColor, ElevationIndex, IconButtonShape};use ui::{Disclosure, Divider};use workspace::dock::{DockPosition, Panel, PanelEvent};use workspace::item::TabContentParams;use workspace::Workspace; pub fn init(cx: &mut AppContext) {    cx.observe_new_views(|workspace: &mut Workspace, _| {        workspace.register_action(|workspace, _: &ToggleFocus, cx| {            workspace.toggle_panel_focus::<GitPanel>(cx);        });    })    .detach();} actions!(    git_panel,    [        Deploy,        DiscardAll,        StageAll,        DiscardSelected,        StageSelected,        UnstageSelected,        UnstageAll,        FilesChanged,        ToggleFocus    ]); const ADDED_COLOR: Hsla = Hsla {    h: 142. / 360.,    s: 0.68,    l: 0.45,    a: 1.0,};const MODIFIED_COLOR: Hsla = Hsla {    h: 48. / 360.,    s: 0.76,    l: 0.47,    a: 1.0,};const REMOVED_COLOR: Hsla = Hsla {    h: 355. / 360.,    s: 0.65,    l: 0.65,    a: 1.0,}; #[derive(Debug, Clone, PartialEq, Eq, Hash)]pub struct ChangedFileId(SharedString); #[derive(Debug, Clone)]pub struct PanelChangedFile {    pub staged: bool,    pub file_path: SharedString,    pub lines_added: usize,    pub lines_removed: usize,    pub status: GitFileStatus,} #[derive(Clone)]pub struct GitLines {    pub added: usize,    pub removed: usize,} struct FileTree {    root: BTreeMap<String, FileTreeNode>,} enum FileTreeNode {    File(ChangedFileId),    Directory(BTreeMap<String, FileTreeNode>),} enum GitListItem {    Header(bool),    Divider,    Directory {        path: String,        indent_level: usize,        is_expanded: bool,    },    File {        id: ChangedFileId,        indent_level: usize,    },} pub struct GitPanelState {    files: HashMap<ChangedFileId, PanelChangedFile>,    file_order: Vec<ChangedFileId>,    file_tree: FileTree,    lines_changed: GitLines,    show_list: bool,    selected_index: usize,    needs_update: bool,    commit_message: Option<SharedString>,} impl GitPanelState {    fn new(changed_files: Vec<PanelChangedFile>) -> Self {        let mut files = HashMap::new();        let mut file_order = Vec::new();        let mut lines_changed = GitLines {            added: 0,            removed: 0,        };         for file in changed_files {            let id = ChangedFileId(file.file_path.clone());            file_order.push(id.clone());            lines_changed.added += file.lines_added;            lines_changed.removed += file.lines_removed;            files.insert(id, file);        }         let file_tree = FileTree::new(&files);         Self {            files,            file_order,            file_tree,            lines_changed,            commit_message: None,            show_list: true,            selected_index: 0,            needs_update: true,        }    }     fn generate_git_list_items(&self) -> Vec<GitListItem> {        let mut items = Vec::new();         // Unstaged header        items.push(GitListItem::Header(false));         // Unstaged files        self.add_files_to_list(&mut items, false);         // Staged header        items.push(GitListItem::Header(true));         // Staged files        self.add_files_to_list(&mut items, true);         items    }     fn add_files_to_list(&self, items: &mut Vec<GitListItem>, is_staged: bool) {        let mut files: Vec<_> = self            .files            .iter()            .filter(|(_, file)| file.staged == is_staged)            .collect();         files.sort_by(|(_, a), (_, b)| a.file_path.cmp(&b.file_path));         for (id, file) in files {            items.push(GitListItem::File {                id: id.clone(),                indent_level: 0,            });        }    }     fn count_files_in_directory(&self, path: &str) -> usize {        self.files            .values()            .filter(|file| file.file_path.starts_with(path))            .count()    }     fn directory_selection(&self, path: &str) -> Selection {        let mut all_staged = true;        let mut all_unstaged = true;         for file in self.files.values() {            if file.file_path.starts_with(path) {                if file.staged {                    all_unstaged = false;                } else {                    all_staged = false;                }                 if !all_staged && !all_unstaged {                    break;                }            }        }         if all_staged {            Selection::Selected        } else if all_unstaged {            Selection::Unselected        } else {            Selection::Indeterminate        }    }     fn changed_file_count(&self) -> usize {        self.files.len()    }     fn unstaged_count(&self) -> usize {        self.files.values().filter(|f| !f.staged).count()    }     fn staged_count(&self) -> usize {        self.files.values().filter(|f| f.staged).count()    }     fn total_item_count(&self) -> usize {        if self.show_list {            self.changed_file_count()        } else {            self.changed_file_count() + 2 // +2 for the two headers        }    }     fn no_unstaged(&self) -> bool {        self.unstaged_count() == 0    }     fn all_unstaged(&self) -> bool {        self.staged_count() == 0    }     fn no_staged(&self) -> bool {        self.staged_count() == 0    }     fn no_changes(&self) -> bool {        self.changed_file_count() == 0    }     fn all_staged(&self) -> bool {        self.unstaged_count() == 0    }     fn update_lines_changed(&mut self) {        self.lines_changed = GitLines {            added: self.files.values().map(|f| f.lines_added).sum(),            removed: self.files.values().map(|f| f.lines_removed).sum(),        };    }     fn discard_all(&mut self) {        self.files.clear();        self.file_order.clear();        self.update_lines_changed();        self.file_tree = FileTree::new(&self.files);    }     fn stage_all(&mut self) {        for file in self.files.values_mut() {            file.staged = true;        }        self.file_tree = FileTree::new(&self.files);    }     fn unstage_all(&mut self) {        for file in self.files.values_mut() {            file.staged = false;        }        self.file_tree = FileTree::new(&self.files);    }     fn discard_selected(&mut self) {        if self.selected_index > 0 && self.selected_index <= self.file_order.len() {            let id = &self.file_order[self.selected_index - 1];            self.files.remove(id);            self.file_order.remove(self.selected_index - 1);            self.update_lines_changed();            self.file_tree = FileTree::new(&self.files);        }    }     fn stage_selected(&mut self) {        if self.selected_index > 0 && self.selected_index <= self.file_order.len() {            let id = &self.file_order[self.selected_index - 1];            if let Some(file) = self.files.get_mut(id) {                file.staged = true;            }            self.file_tree = FileTree::new(&self.files);        }    }     fn unstage_selected(&mut self) {        if self.selected_index > 0 && self.selected_index <= self.file_order.len() {            let id = &self.file_order[self.selected_index - 1];            if let Some(file) = self.files.get_mut(id) {                file.staged = false;            }            self.file_tree = FileTree::new(&self.files);        }    }} impl FileTree {    fn new(files: &HashMap<ChangedFileId, PanelChangedFile>) -> Self {        let mut root = BTreeMap::new();        for (id, file) in files {            let path_parts: Vec<&str> = file.file_path.split('/').collect();            Self::insert_into_tree(&mut root, &path_parts, id);        }        Self { root }    }     fn insert_into_tree(        node: &mut BTreeMap<String, FileTreeNode>,        path_parts: &[&str],        file_id: &ChangedFileId,    ) {        if path_parts.is_empty() {            return;        }         let current = path_parts[0];        if path_parts.len() == 1 {            node.insert(current.to_string(), FileTreeNode::File(file_id.clone()));        } else {            let entry = node                .entry(current.to_string())                .or_insert_with(|| FileTreeNode::Directory(BTreeMap::new()));            if let FileTreeNode::Directory(ref mut child_map) = entry {                Self::insert_into_tree(child_map, &path_parts[1..], file_id);            }        }    }} #[derive(IntoElement)]pub struct ChangedFileItem {    id: ElementId,    file: PanelChangedFile,    is_selected: bool,    indent_level: usize,    model: Model<GitPanelState>,} impl ChangedFileItem {    fn new(        id: impl Into<ElementId>,        file: PanelChangedFile,        is_selected: bool,        indent_level: usize,        model: Model<GitPanelState>,    ) -> Self {        Self {            id: id.into(),            file,            is_selected,            indent_level,            model,        }    }} impl RenderOnce for ChangedFileItem {    fn render(self, cx: &mut WindowContext) -> impl IntoElement {        let file_path = self.file.file_path.clone();         let (icon_name, color) = match self.file.status {            GitFileStatus::Added => (IconName::SquarePlus, Color::Custom(ADDED_COLOR)),            GitFileStatus::Modified => (IconName::SquareDot, Color::Custom(MODIFIED_COLOR)),            GitFileStatus::Conflict => (IconName::SquareMinus, Color::Custom(REMOVED_COLOR)),        };         let is_deleted = self.file.status == GitFileStatus::Conflict;         let selected_color = cx.theme().status().info.opacity(0.1);         h_flex()            .id(self.id.clone())            .items_center()            .justify_between()            .w_full()            .map(|this| {                if self.is_selected {                    this.bg(selected_color)                } else {                    this.bg(cx.theme().colors().elevated_surface_background)                }            })            .hover(|this| this.bg(cx.theme().colors().ghost_element_hover))            .cursor(CursorStyle::PointingHand)            .group("")            .rounded_sm()            .pl(px(12. + (self.indent_level as f32 * 12.)))            .pr(px(12.))            .h(px(28.))            .child(                h_flex()                    .gap(px(8.))                    .child(                        Checkbox::new(self.id.clone(), self.file.staged.into()).on_click({                            let model = self.model.clone();                            let file_path = file_path.clone();                             move |_, cx| {                                let file_path = file_path.clone();                                model.update(cx, |state, cx| {                                    let file =                                        state.files.get_mut(&ChangedFileId(file_path.clone()));                                    if let Some(file) = file {                                        file.staged = !file.staged;                                    }                                    state.update_lines_changed();                                    state.file_tree = FileTree::new(&state.files);                                    state.needs_update = true;                                    cx.notify();                                });                            }                        }),                    )                    .child(Icon::new(icon_name).size(IconSize::Small).color(color))                    .child(                        Label::new(file_path.clone())                            .strikethrough(is_deleted)                            .size(LabelSize::Small)                            .color(if is_deleted {                                Color::Placeholder                            } else {                                Color::Default                            }),                    )                    .child(render_lines_changed(                        self.file.lines_added,                        self.file.lines_removed,                        cx,                    )),            )            .child(                h_flex()                    .gap_2()                    .invisible()                    .group_hover("", |this| this.visible())                    .child(                        IconButton::new("more-menu", IconName::EllipsisVertical)                            .shape(IconButtonShape::Square)                            .size(ButtonSize::Compact)                            .icon_size(IconSize::XSmall)                            .icon_color(Color::Muted),                    ),            )    }} fn render_lines_changed(added: usize, removed: usize, cx: &WindowContext) -> impl IntoElement {    h_flex()        .gap_1()        .text_size(px(10.))        .font_buffer(cx)        .when(added > 0, |this| {            this.child(div().text_color(ADDED_COLOR).child(format!("+{}", added)))        })        .when(removed > 0, |this| {            this.child(                div()                    .text_color(REMOVED_COLOR)                    .child(format!("-{}", removed)),            )        })} #[derive(IntoElement)]pub struct DirectoryItem {    id: ElementId,    path: String,    file_count: usize,    is_selected: bool,    is_expanded: bool,    indent_level: usize,    selection: Selection,} impl DirectoryItem {    fn new(        id: impl Into<ElementId>,        path: String,        file_count: usize,        is_selected: bool,        is_expanded: bool,        indent_level: usize,        selection: Selection,    ) -> Self {        Self {            id: id.into(),            path,            file_count,            is_selected,

Lines 1–500 of 1512.