Nate ButlerUpdate git_panel.rs3abf1651 year agoHistory
884 lines · 30.0 KB · Rust
RawDownload
use anyhow::Context;use collections::HashMap;use editor::Editor;use language::Buffer;use std::{    cell::OnceCell,    collections::HashSet,    ffi::OsStr,    ops::Range,    path::{Path, PathBuf},    sync::Arc,    time::Duration,};use theme::ThemeSettings; use git::repository::GitFileStatus; use util::{ResultExt, TryFutureExt}; use db::kvp::KEY_VALUE_STORE;use gpui::*;use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};use serde::{Deserialize, Serialize};use settings::Settings as _;use ui::{    prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,};use workspace::dock::{DockPosition, Panel, PanelEvent};use workspace::Workspace; use crate::{git_status_icon, settings::GitPanelSettings, GitState};use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll}; actions!(git_panel, [ToggleFocus]); const GIT_PANEL_KEY: &str = "GitPanel"; pub fn init(cx: &mut AppContext) {    cx.observe_new_views(        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {            workspace.register_action(|workspace, _: &ToggleFocus, cx| {                workspace.toggle_panel_focus::<GitPanel>(cx);            });        },    )    .detach();} #[derive(Debug)]pub enum Event {    Focus,} pub struct GitStatusEntry {} #[derive(Debug, PartialEq, Eq, Clone)]struct EntryDetails {    filename: String,    display_name: String,    path: Arc<Path>,    kind: EntryKind,    depth: usize,    is_expanded: bool,    status: Option<GitFileStatus>,} impl EntryDetails {    pub fn is_dir(&self) -> bool {        self.kind.is_dir()    }} #[derive(Serialize, Deserialize)]struct SerializedGitPanel {    width: Option<Pixels>,} pub struct GitPanel {    _workspace: WeakView<Workspace>,    current_modifiers: Modifiers,    focus_handle: FocusHandle,    fs: Arc<dyn Fs>,    hide_scrollbar_task: Option<Task<()>>,    pending_serialization: Task<Option<()>>,    project: Model<Project>,    scroll_handle: UniformListScrollHandle,    scrollbar_state: ScrollbarState,    selected_item: Option<usize>,    show_scrollbar: bool,    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,    git_state: Model<GitState>,    editor: View<Editor>,     // The entries that are currently shown in the panel, aka    // not hidden by folding or such    visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,    width: Option<Pixels>,} impl GitPanel {    pub fn load(        workspace: WeakView<Workspace>,        cx: AsyncWindowContext,    ) -> Task<Result<View<Self>>> {        cx.spawn(|mut cx| async move {            // Clippy incorrectly classifies this as a redundant closure            #[allow(clippy::redundant_closure)]            workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))        })    }     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {        let git_state = GitState::get_global(cx);         let fs = workspace.app_state().fs.clone();        let weak_workspace = workspace.weak_handle();        let project = workspace.project().clone();        let language_registry = workspace.app_state().languages.clone();         let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {            let state = git_state.read(cx);            let current_commit_message = state.commit_message.clone();             let focus_handle = cx.focus_handle();            cx.on_focus(&focus_handle, Self::focus_in).detach();            cx.on_focus_out(&focus_handle, |this, _, cx| {                this.hide_scrollbar(cx);            })            .detach();            cx.subscribe(&project, |this, _project, event, cx| match event {                project::Event::WorktreeRemoved(id) => {                    this.expanded_dir_ids.remove(id);                    this.update_visible_entries(None, cx);                    cx.notify();                }                project::Event::WorktreeUpdatedEntries(_, _)                | project::Event::WorktreeAdded(_)                | project::Event::WorktreeOrderChanged => {                    this.update_visible_entries(None, cx);                    cx.notify();                }                _ => {}            })            .detach();             let editor = cx.new_view(|cx| {                let theme = ThemeSettings::get_global(cx);                 let mut text_style = cx.text_style();                let refinement = TextStyleRefinement {                    font_family: Some(theme.buffer_font.family.clone()),                    font_features: Some(FontFeatures::disable_ligatures()),                    font_size: Some(px(12.).into()),                    color: Some(cx.theme().colors().editor_foreground),                    background_color: Some(gpui::transparent_black()),                    ..Default::default()                };                 text_style.refine(&refinement);                 let mut editor = Editor::auto_height(10, cx);                if let Some(message) = current_commit_message {                    editor.set_text(message, cx);                } else {                    editor.set_text("", cx);                }                // editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);                editor.set_use_autoclose(false);                editor.set_show_gutter(false, cx);                editor.set_show_wrap_guides(false, cx);                editor.set_show_indent_guides(false, cx);                editor.set_text_style_refinement(refinement);                editor.set_placeholder_text("Enter commit message", cx);                editor            });             let buffer = editor                .read(cx)                .buffer()                .read(cx)                .as_singleton()                .expect("commit editor must be singleton");             cx.subscribe(&buffer, Self::on_buffer_event).detach();             let markdown = language_registry.language_for_name("Markdown");            cx.spawn(|_, mut cx| async move {                let markdown = markdown.await.context("failed to load Markdown language")?;                buffer.update(&mut cx, |buffer, cx| {                    buffer.set_language(Some(markdown), cx)                })            })            .detach_and_log_err(cx);             let scroll_handle = UniformListScrollHandle::new();             let mut this = Self {                _workspace: weak_workspace,                focus_handle: cx.focus_handle(),                fs,                pending_serialization: Task::ready(None),                project,                visible_entries: Vec::new(),                current_modifiers: cx.modifiers(),                expanded_dir_ids: Default::default(),                git_state,                editor,                 width: Some(px(360.)),                scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),                scroll_handle,                selected_item: None,                show_scrollbar: !Self::should_autohide_scrollbar(cx),                hide_scrollbar_task: None,            };            this.update_visible_entries(None, cx);            this        });         git_panel    }     fn serialize(&mut self, cx: &mut ViewContext<Self>) {        let width = self.width;        self.pending_serialization = cx.background_executor().spawn(            async move {                KEY_VALUE_STORE                    .write_kvp(                        GIT_PANEL_KEY.into(),                        serde_json::to_string(&SerializedGitPanel { width })?,                    )                    .await?;                anyhow::Ok(())            }            .log_err(),        );    }     fn dispatch_context(&self) -> KeyContext {        let mut dispatch_context = KeyContext::new_with_defaults();        dispatch_context.add("GitPanel");        dispatch_context.add("menu");         dispatch_context    }     fn focus_in(&mut self, cx: &mut ViewContext<Self>) {        if !self.focus_handle.contains_focused(cx) {            cx.emit(Event::Focus);        }    }     fn should_show_scrollbar(_cx: &AppContext) -> bool {        // TODO: plug into settings        true    }     fn should_autohide_scrollbar(_cx: &AppContext) -> bool {        // TODO: plug into settings        true    }     fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);        if !Self::should_autohide_scrollbar(cx) {            return;        }        self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {            cx.background_executor()                .timer(SCROLLBAR_SHOW_INTERVAL)                .await;            panel                .update(&mut cx, |panel, cx| {                    panel.show_scrollbar = false;                    cx.notify();                })                .log_err();        }))    }     fn handle_modifiers_changed(        &mut self,        event: &ModifiersChangedEvent,        cx: &mut ViewContext<Self>,    ) {        self.current_modifiers = event.modifiers;        cx.notify();    }     fn calculate_depth_and_difference(        entry: &Entry,        visible_worktree_entries: &HashSet<Arc<Path>>,    ) -> (usize, usize) {        let (depth, difference) = entry            .path            .ancestors()            .skip(1) // Skip the entry itself            .find_map(|ancestor| {                if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {                    let entry_path_components_count = entry.path.components().count();                    let parent_path_components_count = parent_entry.components().count();                    let difference = entry_path_components_count - parent_path_components_count;                    let depth = parent_entry                        .ancestors()                        .skip(1)                        .filter(|ancestor| visible_worktree_entries.contains(*ancestor))                        .count();                    Some((depth + 1, difference))                } else {                    None                }            })            .unwrap_or((0, 0));         (depth, difference)    }} impl GitPanel {    fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {        // TODO: Implement stage all        println!("Stage all triggered");    }     fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {        // TODO: Implement unstage all        println!("Unstage all triggered");    }     fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {        // TODO: Implement discard all        println!("Discard all triggered");    }     fn clear_message(&mut self, cx: &mut ViewContext<Self>) {        let git_state = self.git_state.clone();        git_state.update(cx, |state, _cx| state.clear_message());        self.editor.update(cx, |editor, cx| editor.set_text("", cx));    }     /// Commit all staged changes    fn commit_staged_changes(&mut self, _: &CommitStagedChanges, cx: &mut ViewContext<Self>) {        self.clear_message(cx);         // TODO: Implement commit all staged        println!("Commit staged changes triggered");    }     /// Commit all changes, regardless of whether they are staged or not    fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext<Self>) {        self.clear_message(cx);         // TODO: Implement commit all changes        println!("Commit all changes triggered");    }     fn all_staged(&self) -> bool {        // TODO: Implement all_staged        true    }     fn no_entries(&self) -> bool {        self.visible_entries.is_empty()    }     fn entry_count(&self) -> usize {        self.visible_entries            .iter()            .map(|(_, entries, _)| {                entries                    .iter()                    .filter(|entry| entry.git_status.is_some())                    .count()            })            .sum()    }     fn for_each_visible_entry(        &self,        range: Range<usize>,        cx: &mut ViewContext<Self>,        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),    ) {        let mut ix = 0;        for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {            if ix >= range.end {                return;            }             if ix + visible_worktree_entries.len() <= range.start {                ix += visible_worktree_entries.len();                continue;            }             let end_ix = range.end.min(ix + visible_worktree_entries.len());            // let entry_range = range.start.saturating_sub(ix)..end_ix - ix;            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {                let snapshot = worktree.read(cx).snapshot();                let root_name = OsStr::new(snapshot.root_name());                let expanded_entry_ids = self                    .expanded_dir_ids                    .get(&snapshot.id())                    .map(Vec::as_slice)                    .unwrap_or(&[]);                 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;                let entries = entries_paths.get_or_init(|| {                    visible_worktree_entries                        .iter()                        .map(|e| (e.path.clone()))                        .collect()                });                 for entry in visible_worktree_entries[entry_range].iter() {                    let status = entry.git_status;                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();                     let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);                     let filename = match difference {                        diff if diff > 1 => entry                            .path                            .iter()                            .skip(entry.path.components().count() - diff)                            .collect::<PathBuf>()                            .to_str()                            .unwrap_or_default()                            .to_string(),                        _ => entry                            .path                            .file_name()                            .map(|name| name.to_string_lossy().into_owned())                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),                    };                     let display_name = entry.path.to_string_lossy().into_owned();                     let details = EntryDetails {                        filename,                        display_name,                        kind: entry.kind,                        is_expanded,                        path: entry.path.clone(),                        status,                        depth,                    };                    callback(entry.id, details, cx);                }            }            ix = end_ix;        }    }     // TODO: Update expanded directory state    fn update_visible_entries(        &mut self,        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,        cx: &mut ViewContext<Self>,    ) {        let project = self.project.read(cx);        self.visible_entries.clear();        for worktree in project.visible_worktrees(cx) {            let snapshot = worktree.read(cx).snapshot();            let worktree_id = snapshot.id();             let mut visible_worktree_entries = Vec::new();            let mut entry_iter = snapshot.entries(true, 0);            while let Some(entry) = entry_iter.entry() {                // Only include entries with a git status                if entry.git_status.is_some() {                    visible_worktree_entries.push(entry.clone());                }                entry_iter.advance();            }             snapshot.propagate_git_statuses(&mut visible_worktree_entries);            project::sort_worktree_entries(&mut visible_worktree_entries);             if !visible_worktree_entries.is_empty() {                self.visible_entries                    .push((worktree_id, visible_worktree_entries, OnceCell::new()));            }        }         if let Some((worktree_id, entry_id)) = new_selected_entry {            self.selected_item = self.visible_entries.iter().enumerate().find_map(                |(worktree_index, (id, entries, _))| {                    if *id == worktree_id {                        entries                            .iter()                            .position(|entry| entry.id == entry_id)                            .map(|entry_index| worktree_index * entries.len() + entry_index)                    } else {                        None                    }                },            );        }         cx.notify();

Lines 1–500 of 884.