directory_context_picker.rs

  1use std::path::Path;
  2use std::sync::atomic::AtomicBool;
  3use std::sync::Arc;
  4
  5use fuzzy::PathMatch;
  6use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
  7use picker::{Picker, PickerDelegate};
  8use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
  9use ui::{prelude::*, ListItem};
 10use util::ResultExt as _;
 11use workspace::{notifications::NotifyResultExt, Workspace};
 12
 13use crate::context_picker::{ConfirmBehavior, ContextPicker};
 14use crate::context_store::ContextStore;
 15
 16pub struct DirectoryContextPicker {
 17    picker: Entity<Picker<DirectoryContextPickerDelegate>>,
 18}
 19
 20impl DirectoryContextPicker {
 21    pub fn new(
 22        context_picker: WeakEntity<ContextPicker>,
 23        workspace: WeakEntity<Workspace>,
 24        context_store: WeakEntity<ContextStore>,
 25        confirm_behavior: ConfirmBehavior,
 26        window: &mut Window,
 27        cx: &mut Context<Self>,
 28    ) -> Self {
 29        let delegate = DirectoryContextPickerDelegate::new(
 30            context_picker,
 31            workspace,
 32            context_store,
 33            confirm_behavior,
 34        );
 35        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 36
 37        Self { picker }
 38    }
 39}
 40
 41impl Focusable for DirectoryContextPicker {
 42    fn focus_handle(&self, cx: &App) -> FocusHandle {
 43        self.picker.focus_handle(cx)
 44    }
 45}
 46
 47impl Render for DirectoryContextPicker {
 48    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 49        self.picker.clone()
 50    }
 51}
 52
 53pub struct DirectoryContextPickerDelegate {
 54    context_picker: WeakEntity<ContextPicker>,
 55    workspace: WeakEntity<Workspace>,
 56    context_store: WeakEntity<ContextStore>,
 57    confirm_behavior: ConfirmBehavior,
 58    matches: Vec<PathMatch>,
 59    selected_index: usize,
 60}
 61
 62impl DirectoryContextPickerDelegate {
 63    pub fn new(
 64        context_picker: WeakEntity<ContextPicker>,
 65        workspace: WeakEntity<Workspace>,
 66        context_store: WeakEntity<ContextStore>,
 67        confirm_behavior: ConfirmBehavior,
 68    ) -> Self {
 69        Self {
 70            context_picker,
 71            workspace,
 72            context_store,
 73            confirm_behavior,
 74            matches: Vec::new(),
 75            selected_index: 0,
 76        }
 77    }
 78
 79    fn search(
 80        &mut self,
 81        query: String,
 82        cancellation_flag: Arc<AtomicBool>,
 83        workspace: &Entity<Workspace>,
 84        cx: &mut Context<Picker<Self>>,
 85    ) -> Task<Vec<PathMatch>> {
 86        if query.is_empty() {
 87            let workspace = workspace.read(cx);
 88            let project = workspace.project().read(cx);
 89            let directory_matches = project.worktrees(cx).flat_map(|worktree| {
 90                let worktree = worktree.read(cx);
 91                let path_prefix: Arc<str> = worktree.root_name().into();
 92                worktree.directories(false, 0).map(move |entry| PathMatch {
 93                    score: 0.,
 94                    positions: Vec::new(),
 95                    worktree_id: worktree.id().to_usize(),
 96                    path: entry.path.clone(),
 97                    path_prefix: path_prefix.clone(),
 98                    distance_to_relative_ancestor: 0,
 99                    is_dir: true,
100                })
101            });
102
103            Task::ready(directory_matches.collect())
104        } else {
105            let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
106            let candidate_sets = worktrees
107                .into_iter()
108                .map(|worktree| {
109                    let worktree = worktree.read(cx);
110
111                    PathMatchCandidateSet {
112                        snapshot: worktree.snapshot(),
113                        include_ignored: worktree
114                            .root_entry()
115                            .map_or(false, |entry| entry.is_ignored),
116                        include_root_name: true,
117                        candidates: project::Candidates::Directories,
118                    }
119                })
120                .collect::<Vec<_>>();
121
122            let executor = cx.background_executor().clone();
123            cx.foreground_executor().spawn(async move {
124                fuzzy::match_path_sets(
125                    candidate_sets.as_slice(),
126                    query.as_str(),
127                    None,
128                    false,
129                    100,
130                    &cancellation_flag,
131                    executor,
132                )
133                .await
134            })
135        }
136    }
137}
138
139impl PickerDelegate for DirectoryContextPickerDelegate {
140    type ListItem = ListItem;
141
142    fn match_count(&self) -> usize {
143        self.matches.len()
144    }
145
146    fn selected_index(&self) -> usize {
147        self.selected_index
148    }
149
150    fn set_selected_index(
151        &mut self,
152        ix: usize,
153        _window: &mut Window,
154        _cx: &mut Context<Picker<Self>>,
155    ) {
156        self.selected_index = ix;
157    }
158
159    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
160        "Search folders…".into()
161    }
162
163    fn update_matches(
164        &mut self,
165        query: String,
166        _window: &mut Window,
167        cx: &mut Context<Picker<Self>>,
168    ) -> Task<()> {
169        let Some(workspace) = self.workspace.upgrade() else {
170            return Task::ready(());
171        };
172
173        let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
174
175        cx.spawn(|this, mut cx| async move {
176            let mut paths = search_task.await;
177            let empty_path = Path::new("");
178            paths.retain(|path_match| path_match.path.as_ref() != empty_path);
179
180            this.update(&mut cx, |this, _cx| {
181                this.delegate.matches = paths;
182            })
183            .log_err();
184        })
185    }
186
187    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
188        let Some(mat) = self.matches.get(self.selected_index) else {
189            return;
190        };
191
192        let project_path = ProjectPath {
193            worktree_id: WorktreeId::from_usize(mat.worktree_id),
194            path: mat.path.clone(),
195        };
196
197        let Some(task) = self
198            .context_store
199            .update(cx, |context_store, cx| {
200                context_store.add_directory(project_path, cx)
201            })
202            .ok()
203        else {
204            return;
205        };
206
207        let confirm_behavior = self.confirm_behavior;
208        cx.spawn_in(window, |this, mut cx| async move {
209            match task.await.notify_async_err(&mut cx) {
210                None => anyhow::Ok(()),
211                Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior {
212                    ConfirmBehavior::KeepOpen => {}
213                    ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
214                }),
215            }
216        })
217        .detach_and_log_err(cx);
218    }
219
220    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
221        self.context_picker
222            .update(cx, |_, cx| {
223                cx.emit(DismissEvent);
224            })
225            .ok();
226    }
227
228    fn render_match(
229        &self,
230        ix: usize,
231        selected: bool,
232        _window: &mut Window,
233        cx: &mut Context<Picker<Self>>,
234    ) -> Option<Self::ListItem> {
235        let path_match = &self.matches[ix];
236        let directory_name = path_match.path.to_string_lossy().to_string();
237
238        let added = self.context_store.upgrade().map_or(false, |context_store| {
239            context_store
240                .read(cx)
241                .includes_directory(&path_match.path)
242                .is_some()
243        });
244
245        Some(
246            ListItem::new(ix)
247                .inset(true)
248                .toggle_state(selected)
249                .start_slot(
250                    Icon::new(IconName::Folder)
251                        .size(IconSize::XSmall)
252                        .color(Color::Muted),
253                )
254                .child(Label::new(directory_name))
255                .when(added, |el| {
256                    el.end_slot(
257                        h_flex()
258                            .gap_1()
259                            .child(
260                                Icon::new(IconName::Check)
261                                    .size(IconSize::Small)
262                                    .color(Color::Success),
263                            )
264                            .child(Label::new("Added").size(LabelSize::Small)),
265                    )
266                }),
267        )
268    }
269}