Skip to repository content241 lines · 7.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:40:27.551Z 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
picker_prompt.rs
1use futures::channel::oneshot;
2use fuzzy::{StringMatch, StringMatchCandidate};
3
4use core::cmp;
5use gpui::{
6 App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
7 IntoElement, ParentElement, Render, SharedString, Subscription, Task, WeakEntity, Window, rems,
8};
9use picker::{Picker, PickerDelegate};
10use std::sync::Arc;
11use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
12use util::ResultExt;
13use workspace::{ModalView, Workspace};
14
15pub struct PickerPrompt {
16 pub picker: Entity<Picker<PickerPromptDelegate>>,
17 _subscription: Subscription,
18}
19
20pub fn prompt(
21 prompt: &str,
22 options: Vec<SharedString>,
23 workspace: WeakEntity<Workspace>,
24 window: &mut Window,
25 cx: &mut App,
26) -> Task<Option<usize>> {
27 if options.is_empty() {
28 return Task::ready(None);
29 } else if options.len() == 1 {
30 return Task::ready(Some(0));
31 }
32 let prompt = prompt.to_string().into();
33
34 window.spawn(cx, async move |cx| {
35 // Modal branch picker has a longer trailoff than a popover one.
36 let (tx, rx) = oneshot::channel();
37 let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
38
39 workspace
40 .update_in(cx, |workspace, window, cx| {
41 workspace.toggle_modal(window, cx, |window, cx| {
42 PickerPrompt::new(delegate, 34., window, cx)
43 })
44 })
45 .ok();
46
47 (rx.await).ok()
48 })
49}
50
51impl PickerPrompt {
52 fn new(
53 delegate: PickerPromptDelegate,
54 rem_width: f32,
55 window: &mut Window,
56 cx: &mut Context<Self>,
57 ) -> Self {
58 let picker =
59 cx.new(|cx| Picker::uniform_list(delegate, window, cx).initial_width(rems(rem_width)));
60 let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
61 Self {
62 picker,
63 _subscription,
64 }
65 }
66}
67impl ModalView for PickerPrompt {}
68impl EventEmitter<DismissEvent> for PickerPrompt {}
69
70impl Focusable for PickerPrompt {
71 fn focus_handle(&self, cx: &App) -> FocusHandle {
72 self.picker.focus_handle(cx)
73 }
74}
75
76impl Render for PickerPrompt {
77 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
78 v_flex()
79 .child(self.picker.clone())
80 .on_mouse_down_out(cx.listener(|this, _, window, cx| {
81 this.picker.update(cx, |this, cx| {
82 this.cancel(&Default::default(), window, cx);
83 })
84 }))
85 }
86}
87
88pub struct PickerPromptDelegate {
89 prompt: Arc<str>,
90 matches: Vec<StringMatch>,
91 all_options: Vec<SharedString>,
92 selected_index: usize,
93 max_match_length: usize,
94 tx: Option<oneshot::Sender<usize>>,
95}
96
97impl PickerPromptDelegate {
98 pub fn new(
99 prompt: Arc<str>,
100 options: Vec<SharedString>,
101 tx: oneshot::Sender<usize>,
102 max_chars: usize,
103 ) -> Self {
104 Self {
105 prompt,
106 all_options: options,
107 matches: vec![],
108 selected_index: 0,
109 max_match_length: max_chars,
110 tx: Some(tx),
111 }
112 }
113}
114
115impl PickerDelegate for PickerPromptDelegate {
116 type ListItem = ListItem;
117
118 fn name() -> &'static str {
119 "picker prompt"
120 }
121
122 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
123 self.prompt.clone()
124 }
125
126 fn match_count(&self) -> usize {
127 self.matches.len()
128 }
129
130 fn selected_index(&self) -> usize {
131 self.selected_index
132 }
133
134 fn set_selected_index(
135 &mut self,
136 ix: usize,
137 _window: &mut Window,
138 _: &mut Context<Picker<Self>>,
139 ) {
140 self.selected_index = ix;
141 }
142
143 fn update_matches(
144 &mut self,
145 query: String,
146 window: &mut Window,
147 cx: &mut Context<Picker<Self>>,
148 ) -> Task<()> {
149 cx.spawn_in(window, async move |picker, cx| {
150 let candidates = picker.read_with(cx, |picker, _| {
151 picker
152 .delegate
153 .all_options
154 .iter()
155 .enumerate()
156 .map(|(ix, option)| StringMatchCandidate::new(ix, option))
157 .collect::<Vec<StringMatchCandidate>>()
158 });
159 let Some(candidates) = candidates.log_err() else {
160 return;
161 };
162 let matches: Vec<StringMatch> = if query.is_empty() {
163 candidates
164 .into_iter()
165 .enumerate()
166 .map(|(index, candidate)| StringMatch {
167 candidate_id: index,
168 string: candidate.string,
169 positions: Vec::new(),
170 score: 0.0,
171 })
172 .collect()
173 } else {
174 fuzzy::match_strings(
175 &candidates,
176 &query,
177 true,
178 true,
179 10000,
180 &Default::default(),
181 cx.background_executor().clone(),
182 )
183 .await
184 };
185 picker
186 .update(cx, |picker, _| {
187 let delegate = &mut picker.delegate;
188 delegate.matches = matches;
189 if delegate.matches.is_empty() {
190 delegate.selected_index = 0;
191 } else {
192 delegate.selected_index =
193 cmp::min(delegate.selected_index, delegate.matches.len() - 1);
194 }
195 })
196 .log_err();
197 })
198 }
199
200 fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
201 let Some(option) = self.matches.get(self.selected_index()) else {
202 return;
203 };
204
205 self.tx.take().map(|tx| tx.send(option.candidate_id));
206 cx.emit(DismissEvent);
207 }
208
209 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
210 cx.emit(DismissEvent);
211 }
212
213 fn render_match(
214 &self,
215 ix: usize,
216 selected: bool,
217 _window: &mut Window,
218 _cx: &mut Context<Picker<Self>>,
219 ) -> Option<Self::ListItem> {
220 let hit = &self.matches.get(ix)?;
221 let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
222
223 Some(
224 ListItem::new(format!("picker-prompt-menu-{ix}"))
225 .inset(true)
226 .spacing(ListItemSpacing::Sparse)
227 .toggle_state(selected)
228 .map(|el| {
229 let highlights: Vec<_> = hit
230 .positions
231 .iter()
232 .filter(|&&index| index < self.max_match_length)
233 .copied()
234 .collect();
235
236 el.child(HighlightedLabel::new(shortened_option, highlights))
237 }),
238 )
239 }
240}
241