789 lines · 26.5 KB · Rust
RawDownload
use fuzzy::StringMatchCandidate; use git::stash::StashEntry;use gpui::{    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,    SharedString, Styled, Subscription, Task, TaskExt, WeakEntity, Window, actions, rems,};use picker::{Picker, PickerDelegate};use project::git_store::{Repository, RepositoryEvent};use std::sync::Arc;use time::{OffsetDateTime, UtcOffset};use time_format;use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};use util::ResultExt;use workspace::notifications::DetachAndPromptErr;use workspace::{ModalView, Workspace}; use crate::commit_view::CommitView;use crate::stash_picker; actions!(    stash_picker,    [        /// Drop the selected stash entry.        DropStashItem,        /// Show the diff view of the selected stash entry.        ShowStashItem,    ]); pub fn open(    workspace: &mut Workspace,    _: &zed_actions::git::ViewStash,    window: &mut Window,    cx: &mut Context<Workspace>,) {    let repository = workspace.project().read(cx).active_repository(cx);    let weak_workspace = workspace.weak_handle();    workspace.toggle_modal(window, cx, |window, cx| {        StashList::new(repository, weak_workspace, rems(34.), window, cx)    })} pub fn create_embedded(    repository: Option<Entity<Repository>>,    workspace: WeakEntity<Workspace>,    width: Rems,    show_footer: bool,    window: &mut Window,    cx: &mut Context<StashList>,) -> StashList {    StashList::new_embedded(repository, workspace, width, show_footer, window, cx)} pub struct StashList {    width: Rems,    pub picker: Entity<Picker<StashListDelegate>>,    picker_focus_handle: FocusHandle,    _subscriptions: Vec<Subscription>,} impl StashList {    fn new(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);        this._subscriptions            .push(cx.subscribe(&this.picker, |_, _, _, cx| {                cx.emit(DismissEvent);            }));        this    }     fn new_inner(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        embedded: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let mut _subscriptions = Vec::new();        let stash_request = repository            .clone()            .map(|repository| repository.read_with(cx, |repo, _| repo.cached_stash()));         if let Some(repo) = repository.clone() {            _subscriptions.push(                cx.subscribe_in(&repo, window, |this, _, event, window, cx| {                    if matches!(event, RepositoryEvent::StashEntriesChanged) {                        let stash_entries = this.picker.read_with(cx, |picker, cx| {                            picker                                .delegate                                .repo                                .clone()                                .map(|repo| repo.read(cx).cached_stash().entries.to_vec())                        });                        this.picker.update(cx, |this, cx| {                            this.delegate.all_stash_entries = stash_entries;                            this.refresh(window, cx);                        });                    }                }),            )        }         cx.spawn_in(window, async move |this, cx| {            let stash_entries = stash_request                .map(|git_stash| git_stash.entries.to_vec())                .unwrap_or_default();             this.update_in(cx, |this, window, cx| {                this.picker.update(cx, |picker, cx| {                    picker.delegate.all_stash_entries = Some(stash_entries);                    picker.refresh(window, cx);                })            })?;             anyhow::Ok(())        })        .detach_and_log_err(cx);         let delegate = StashListDelegate::new(repository, workspace, window, cx);        let picker = cx.new(|cx| {            Picker::uniform_list(delegate, window, cx)                .initial_width(width)                .show_scrollbar(true)                .when(embedded, |picker| picker.embedded())        });        let picker_focus_handle = picker.focus_handle(cx);        picker.update(cx, |picker, _| {            picker.delegate.focus_handle = picker_focus_handle.clone();            picker.delegate.show_footer = !embedded;        });         Self {            picker,            picker_focus_handle,            width,            _subscriptions,        }    }     fn new_embedded(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        show_footer: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);        this.picker.update(cx, |picker, _| {            picker.delegate.show_footer = show_footer;        });        this._subscriptions            .push(cx.subscribe(&this.picker, |_, _, _, cx| {                cx.emit(DismissEvent);            }));        this    }     pub fn handle_drop_stash(        &mut self,        _: &DropStashItem,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker.update(cx, |picker, cx| {            picker                .delegate                .drop_stash_at(picker.delegate.selected_index(), window, cx);        });        cx.notify();    }     pub fn handle_show_stash(        &mut self,        _: &ShowStashItem,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker.update(cx, |picker, cx| {            picker                .delegate                .show_stash_at(picker.delegate.selected_index(), window, cx);        });         cx.emit(DismissEvent);    }     pub fn handle_modifiers_changed(        &mut self,        ev: &ModifiersChangedEvent,        _: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)    }} impl ModalView for StashList {}impl EventEmitter<DismissEvent> for StashList {}impl Focusable for StashList {    fn focus_handle(&self, _: &App) -> FocusHandle {        self.picker_focus_handle.clone()    }} impl Render for StashList {    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        v_flex()            .key_context("StashList")            .w(self.width)            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))            .on_action(cx.listener(Self::handle_drop_stash))            .on_action(cx.listener(Self::handle_show_stash))            .child(self.picker.clone())    }} #[derive(Debug, Clone)]struct StashEntryMatch {    entry: StashEntry,    positions: Vec<usize>,    formatted_timestamp: String,    formatted_absolute_timestamp: String,} pub struct StashListDelegate {    matches: Vec<StashEntryMatch>,    all_stash_entries: Option<Vec<StashEntry>>,    repo: Option<Entity<Repository>>,    workspace: WeakEntity<Workspace>,    selected_index: usize,    last_query: String,    modifiers: Modifiers,    focus_handle: FocusHandle,    timezone: UtcOffset,    show_footer: bool,} impl StashListDelegate {    fn new(        repo: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        _window: &mut Window,        cx: &mut Context<StashList>,    ) -> Self {        let timezone = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);         Self {            matches: vec![],            repo,            workspace,            all_stash_entries: None,            selected_index: 0,            last_query: Default::default(),            modifiers: Default::default(),            focus_handle: cx.focus_handle(),            timezone,            show_footer: false,        }    }     fn format_message(ix: usize, message: &String) -> String {        format!("#{}: {}", ix, message)    }     fn format_timestamp(timestamp: i64, timezone: UtcOffset) -> String {        let timestamp =            OffsetDateTime::from_unix_timestamp(timestamp).unwrap_or(OffsetDateTime::now_utc());        time_format::format_localized_timestamp(            timestamp,            OffsetDateTime::now_utc(),            timezone,            time_format::TimestampFormat::Relative,        )    }     fn format_absolute_timestamp(timestamp: i64, timezone: UtcOffset) -> String {        let timestamp =            OffsetDateTime::from_unix_timestamp(timestamp).unwrap_or(OffsetDateTime::now_utc());        time_format::format_localized_timestamp(            timestamp,            OffsetDateTime::now_utc(),            timezone,            time_format::TimestampFormat::EnhancedAbsolute,        )    }     fn drop_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(entry_match) = self.matches.get(ix) else {            return;        };        let stash_index = entry_match.entry.index;        let Some(repo) = self.repo.clone() else {            return;        };         cx.spawn(async move |_, cx| {            repo.update(cx, |repo, cx| repo.stash_drop(Some(stash_index), cx))                .await??;            Ok(())        })        .detach_and_prompt_err("Failed to drop stash", window, cx, |e, _, _| {            Some(e.to_string())        });    }     fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(entry_match) = self.matches.get(ix) else {            return;        };        let stash_sha = entry_match.entry.oid.to_string();        let stash_index = entry_match.entry.index;        let Some(repo) = self.repo.clone() else {            return;        };        CommitView::open(            stash_sha,            repo.downgrade(),            self.workspace.clone(),            Some(stash_index),            None,            window,            cx,        );    }     fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(repo) = self.repo.clone() else {            return;        };         cx.spawn(async move |_, cx| {            repo.update(cx, |repo, cx| repo.stash_pop(Some(stash_index), cx))                .await?;            Ok(())        })        .detach_and_prompt_err("Failed to pop stash", window, cx, |e, _, _| {            Some(e.to_string())        });        cx.emit(DismissEvent);    }     fn apply_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(repo) = self.repo.clone() else {            return;        };         cx.spawn(async move |_, cx| {            repo.update(cx, |repo, cx| repo.stash_apply(Some(stash_index), cx))                .await?;            Ok(())        })        .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| {            Some(e.to_string())        });        cx.emit(DismissEvent);    }} impl PickerDelegate for StashListDelegate {    type ListItem = ListItem;     fn name() -> &'static str {        "stash picker"    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Select a stash…".into()    }     fn match_count(&self) -> usize {        self.matches.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn update_matches(        &mut self,        query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let Some(all_stash_entries) = self.all_stash_entries.clone() else {            return Task::ready(());        };         let timezone = self.timezone;         cx.spawn_in(window, async move |picker, cx| {            let matches: Vec<StashEntryMatch> = if query.is_empty() {                all_stash_entries                    .into_iter()                    .map(|entry| {                        let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone);                        let formatted_absolute_timestamp =                            Self::format_absolute_timestamp(entry.timestamp, timezone);                         StashEntryMatch {                            entry,                            positions: Vec::new(),                            formatted_timestamp,                            formatted_absolute_timestamp,                        }                    })                    .collect()            } else {                let candidates = all_stash_entries                    .iter()                    .enumerate()                    .map(|(ix, entry)| {                        StringMatchCandidate::new(                            ix,                            &Self::format_message(entry.index, &entry.message),                        )                    })                    .collect::<Vec<StringMatchCandidate>>();                fuzzy::match_strings(                    &candidates,                    &query,                    false,                    true,                    10000,                    &Default::default(),                    cx.background_executor().clone(),                )                .await                .into_iter()                .map(|candidate| {                    let entry = all_stash_entries[candidate.candidate_id].clone();                    let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone);                    let formatted_absolute_timestamp =                        Self::format_absolute_timestamp(entry.timestamp, timezone);                     StashEntryMatch {                        entry,                        positions: candidate.positions,                        formatted_timestamp,                        formatted_absolute_timestamp,                    }                })                .collect()            };             picker                .update(cx, |picker, _| {                    let delegate = &mut picker.delegate;                    delegate.matches = matches;                    if delegate.matches.is_empty() {                        delegate.selected_index = 0;                    } else {                        delegate.selected_index =                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);                    }                    delegate.last_query = query;                })                .log_err();        })    }     fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(entry_match) = self.matches.get(self.selected_index()) else {            return;        };        let stash_index = entry_match.entry.index;        if secondary {            self.pop_stash(stash_index, window, cx);        } else {            self.apply_stash(stash_index, window, cx);        }    }     fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {        cx.emit(DismissEvent);    }     fn render_match(        &self,        ix: usize,        selected: bool,

Lines 1–500 of 789.