directory_context_picker.rs

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