Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:27.334Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

sidebar_recent_projects.rs

442 lines · 15.6 KB · rust
1use std::sync::Arc;
2
3use fuzzy_nucleo::{StringMatch, StringMatchCandidate, match_strings};
4use gpui::{
5    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
6    Subscription, Task, TaskExt, WeakEntity, Window,
7};
8use picker::{
9    Picker, PickerDelegate,
10    highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
11};
12use remote::RemoteConnectionOptions;
13use settings::Settings;
14use ui::{ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
15use util::{ResultExt, paths::PathExt};
16use workspace::{
17    MultiWorkspace, OpenMode, OpenOptions, ProjectGroupKey, RecentWorkspace,
18    SerializedWorkspaceLocation, Workspace, WorkspaceDb, notifications::DetachAndPromptErr,
19};
20
21use zed_actions::OpenRemote;
22
23use crate::{highlights_for_path, icon_for_remote_connection, open_remote_project};
24
25pub struct SidebarRecentProjects {
26    pub picker: Entity<Picker<SidebarRecentProjectsDelegate>>,
27    _subscription: Subscription,
28}
29
30impl SidebarRecentProjects {
31    pub fn popover(
32        workspace: WeakEntity<Workspace>,
33        window_project_groups: Vec<ProjectGroupKey>,
34        _focus_handle: FocusHandle,
35        window: &mut Window,
36        cx: &mut App,
37    ) -> Entity<Self> {
38        let fs = workspace
39            .upgrade()
40            .map(|ws| ws.read(cx).app_state().fs.clone());
41
42        cx.new(|cx| {
43            let delegate = SidebarRecentProjectsDelegate {
44                workspace,
45                window_project_groups,
46                workspaces: Vec::new(),
47                filtered_workspaces: Vec::new(),
48                selected_index: 0,
49                has_any_non_local_projects: false,
50                focus_handle: cx.focus_handle(),
51            };
52
53            let picker: Entity<Picker<SidebarRecentProjectsDelegate>> = cx.new(|cx| {
54                Picker::list(delegate, window, cx)
55                    .list_measure_all()
56                    .show_scrollbar(true)
57                    .initial_width(rems(18.))
58                    .popover()
59            });
60
61            let picker_focus_handle = picker.focus_handle(cx);
62            picker.update(cx, |picker, _| {
63                picker.delegate.focus_handle = picker_focus_handle;
64            });
65
66            let _subscription =
67                cx.subscribe(&picker, |_this: &mut Self, _, _, cx| cx.emit(DismissEvent));
68
69            let db = WorkspaceDb::global(cx);
70            cx.spawn_in(window, async move |this, cx| {
71                let Some(fs) = fs else { return };
72                let workspaces = db
73                    .recent_project_workspaces(fs.as_ref())
74                    .await
75                    .log_err()
76                    .unwrap_or_default();
77                this.update_in(cx, move |this, window, cx| {
78                    this.picker.update(cx, move |picker, cx| {
79                        picker.delegate.set_workspaces(workspaces);
80                        picker.update_matches(picker.query(cx), window, cx)
81                    })
82                })
83                .ok();
84            })
85            .detach();
86
87            picker.focus_handle(cx).focus(window, cx);
88
89            Self {
90                picker,
91                _subscription,
92            }
93        })
94    }
95}
96
97impl EventEmitter<DismissEvent> for SidebarRecentProjects {}
98
99impl Focusable for SidebarRecentProjects {
100    fn focus_handle(&self, cx: &App) -> FocusHandle {
101        self.picker.focus_handle(cx)
102    }
103}
104
105impl Render for SidebarRecentProjects {
106    fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
107        v_flex()
108            .key_context("SidebarRecentProjects")
109            .w(rems(18.))
110            .child(self.picker.clone())
111    }
112}
113
114pub struct SidebarRecentProjectsDelegate {
115    workspace: WeakEntity<Workspace>,
116    window_project_groups: Vec<ProjectGroupKey>,
117    workspaces: Vec<RecentWorkspace>,
118    filtered_workspaces: Vec<StringMatch>,
119    selected_index: usize,
120    has_any_non_local_projects: bool,
121    focus_handle: FocusHandle,
122}
123
124impl SidebarRecentProjectsDelegate {
125    pub fn set_workspaces(&mut self, workspaces: Vec<RecentWorkspace>) {
126        self.has_any_non_local_projects = workspaces
127            .iter()
128            .any(|workspace| !matches!(workspace.location, SerializedWorkspaceLocation::Local));
129        self.workspaces = workspaces;
130    }
131}
132
133impl EventEmitter<DismissEvent> for SidebarRecentProjectsDelegate {}
134
135impl PickerDelegate for SidebarRecentProjectsDelegate {
136    type ListItem = AnyElement;
137
138    fn name() -> &'static str {
139        "sidebar recent projects"
140    }
141
142    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
143        "Search projects…".into()
144    }
145
146    fn match_count(&self) -> usize {
147        self.filtered_workspaces.len()
148    }
149
150    fn selected_index(&self) -> usize {
151        self.selected_index
152    }
153
154    fn set_selected_index(
155        &mut self,
156        ix: usize,
157        _window: &mut Window,
158        _cx: &mut Context<Picker<Self>>,
159    ) {
160        self.selected_index = ix;
161    }
162
163    fn update_matches(
164        &mut self,
165        query: String,
166        _: &mut Window,
167        cx: &mut Context<Picker<Self>>,
168    ) -> Task<()> {
169        let query = query.trim_start();
170        let case = fuzzy_nucleo::Case::smart_if_uppercase_in(query);
171        let is_empty_query = query.is_empty();
172
173        let current_workspace_id = self
174            .workspace
175            .upgrade()
176            .and_then(|ws| ws.read(cx).database_id());
177
178        let candidates: Vec<_> = self
179            .workspaces
180            .iter()
181            .enumerate()
182            .filter(|(_, workspace)| {
183                Some(workspace.workspace_id) != current_workspace_id
184                    && !self
185                        .window_project_groups
186                        .iter()
187                        .any(|key| key.matches(&workspace.project_group_key()))
188            })
189            .map(|(id, workspace)| {
190                let combined_string = workspace
191                    .identity_paths
192                    .ordered_paths()
193                    .map(|path| path.compact().to_string_lossy().into_owned())
194                    .collect::<Vec<_>>()
195                    .concat();
196                StringMatchCandidate::new(id, &combined_string)
197            })
198            .collect();
199
200        if is_empty_query {
201            self.filtered_workspaces = candidates
202                .into_iter()
203                .map(|candidate| StringMatch {
204                    candidate_id: candidate.id,
205                    score: 0.0,
206                    positions: Vec::new(),
207                    string: candidate.string,
208                })
209                .collect();
210        } else {
211            self.filtered_workspaces = match_strings(
212                &candidates,
213                query,
214                case,
215                fuzzy_nucleo::LengthPenalty::On,
216                100,
217            );
218        }
219
220        self.selected_index = 0;
221        Task::ready(())
222    }
223
224    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
225        let Some(hit) = self.filtered_workspaces.get(self.selected_index) else {
226            return;
227        };
228        let Some(recent_workspace) = self.workspaces.get(hit.candidate_id) else {
229            return;
230        };
231
232        let Some(workspace) = self.workspace.upgrade() else {
233            return;
234        };
235
236        match &recent_workspace.location {
237            SerializedWorkspaceLocation::Local => {
238                if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
239                    let paths = recent_workspace.paths.paths().to_vec();
240                    cx.defer(move |cx| {
241                        if let Some(task) = handle
242                            .update(cx, |multi_workspace, window, cx| {
243                                multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
244                            })
245                            .log_err()
246                        {
247                            task.detach_and_log_err(cx);
248                        }
249                    });
250                }
251            }
252            SerializedWorkspaceLocation::Remote(connection) => {
253                let mut connection = connection.clone();
254                workspace.update(cx, |workspace, cx| {
255                    let app_state = workspace.app_state().clone();
256                    let replace_window = window.window_handle().downcast::<MultiWorkspace>();
257                    let open_options = OpenOptions {
258                        requesting_window: replace_window,
259                        ..Default::default()
260                    };
261                    if let RemoteConnectionOptions::Ssh(connection) = &mut connection {
262                        crate::RemoteSettings::get_global(cx)
263                            .fill_connection_options_from_settings(connection);
264                    };
265                    let paths = recent_workspace.paths.paths().to_vec();
266                    cx.spawn_in(window, async move |_, cx| {
267                        open_remote_project(connection.clone(), paths, app_state, open_options, cx)
268                            .await
269                    })
270                    .detach_and_prompt_err(
271                        "Failed to open project",
272                        window,
273                        cx,
274                        |_, _, _| None,
275                    );
276                });
277            }
278        }
279        cx.emit(DismissEvent);
280    }
281
282    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
283
284    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
285        let text = if self.workspaces.is_empty() {
286            "Recently opened projects will show up here"
287        } else {
288            "No matches"
289        };
290        Some(text.into())
291    }
292
293    fn render_match(
294        &self,
295        ix: usize,
296        selected: bool,
297        window: &mut Window,
298        cx: &mut Context<Picker<Self>>,
299    ) -> Option<Self::ListItem> {
300        let hit = self.filtered_workspaces.get(ix)?;
301        let workspace = self.workspaces.get(hit.candidate_id)?;
302
303        let ordered_paths: Vec<_> = workspace
304            .identity_paths
305            .ordered_paths()
306            .map(|p| p.compact().to_string_lossy().to_string())
307            .collect();
308
309        let tooltip_path: SharedString = match &workspace.location {
310            SerializedWorkspaceLocation::Remote(options) => {
311                let host = options.display_name();
312                if ordered_paths.len() == 1 {
313                    format!("{} ({})", ordered_paths[0], host).into()
314                } else {
315                    format!("{}\n({})", ordered_paths.join("\n"), host).into()
316                }
317            }
318            _ => ordered_paths.join("\n").into(),
319        };
320
321        let mut path_start_offset = 0;
322        let match_labels: Vec<_> = workspace
323            .identity_paths
324            .ordered_paths()
325            .map(|p| p.compact())
326            .map(|path| {
327                let (label, path_match) =
328                    highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);
329                path_start_offset += path_match.text.len();
330                label
331            })
332            .collect();
333
334        let prefix = match &workspace.location {
335            SerializedWorkspaceLocation::Remote(options) => {
336                Some(SharedString::from(options.display_name()))
337            }
338            _ => None,
339        };
340
341        let highlighted_match = HighlightedMatchWithPaths {
342            prefix,
343            match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
344            paths: Vec::new(),
345            active: false,
346        };
347
348        let icon = icon_for_remote_connection(match &workspace.location {
349            SerializedWorkspaceLocation::Local => None,
350            SerializedWorkspaceLocation::Remote(options) => Some(options),
351        });
352
353        Some(
354            ListItem::new(ix)
355                .toggle_state(selected)
356                .inset(true)
357                .spacing(ListItemSpacing::Sparse)
358                .child(
359                    h_flex()
360                        .w_full()
361                        .min_w_0()
362                        .gap_3()
363                        .flex_grow_1()
364                        .when(self.has_any_non_local_projects, |this| {
365                            this.child(Icon::new(icon).color(Color::Muted))
366                        })
367                        .child(highlighted_match.render(window, cx)),
368                )
369                .tooltip(move |_, cx| {
370                    Tooltip::with_meta(
371                        "Open Project in This Window",
372                        None,
373                        tooltip_path.clone(),
374                        cx,
375                    )
376                })
377                .into_any_element(),
378        )
379    }
380
381    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
382        let focus_handle = self.focus_handle.clone();
383
384        Some(
385            v_flex()
386                .p_1p5()
387                .flex_1()
388                .gap_1()
389                .border_t_1()
390                .border_color(cx.theme().colors().border_variant)
391                .child({
392                    let open_action = workspace::Open {
393                        create_new_window: Some(false),
394                    };
395
396                    ButtonLike::new("open_local_folder")
397                        .child(
398                            h_flex()
399                                .w_full()
400                                .gap_1()
401                                .justify_between()
402                                .child(Label::new("Open Local Folders"))
403                                .child(KeyBinding::for_action_in(&open_action, &focus_handle, cx)),
404                        )
405                        .on_click(cx.listener(move |_, _, window, cx| {
406                            window.dispatch_action(open_action.boxed_clone(), cx);
407                            cx.emit(DismissEvent);
408                        }))
409                })
410                .child(
411                    ButtonLike::new("open_remote_folder")
412                        .child(
413                            h_flex()
414                                .w_full()
415                                .gap_1()
416                                .justify_between()
417                                .child(Label::new("Open Remote Folder"))
418                                .child(KeyBinding::for_action(
419                                    &OpenRemote {
420                                        from_existing_connection: false,
421                                        create_new_window: Some(false),
422                                    },
423                                    cx,
424                                )),
425                        )
426                        .on_click(cx.listener(|_, _, window, cx| {
427                            window.dispatch_action(
428                                OpenRemote {
429                                    from_existing_connection: false,
430                                    create_new_window: Some(false),
431                                }
432                                .boxed_clone(),
433                                cx,
434                            );
435                            cx.emit(DismissEvent);
436                        })),
437                )
438                .into_any(),
439        )
440    }
441}
442
Served at tenant.openagents/omega Member data and write actions are omitted.