Skip to repository content442 lines · 15.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:33:16.582Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
sidebar_recent_projects.rs
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