1use std::str::FromStr;
2use std::sync::Arc;
3
4use client::ExtensionMetadata;
5use extension_host::{ExtensionSettings, ExtensionStore};
6use fs::Fs;
7use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
8use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*};
9use picker::{Picker, PickerDelegate};
10use release_channel::ReleaseChannel;
11use semantic_version::SemanticVersion;
12use settings::update_settings_file;
13use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
14use util::ResultExt;
15use workspace::ModalView;
16
17pub struct ExtensionVersionSelector {
18 picker: Entity<Picker<ExtensionVersionSelectorDelegate>>,
19}
20
21impl ModalView for ExtensionVersionSelector {}
22
23impl EventEmitter<DismissEvent> for ExtensionVersionSelector {}
24
25impl Focusable for ExtensionVersionSelector {
26 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
27 self.picker.focus_handle(cx)
28 }
29}
30
31impl Render for ExtensionVersionSelector {
32 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
33 v_flex().w(rems(34.)).child(self.picker.clone())
34 }
35}
36
37impl ExtensionVersionSelector {
38 pub fn new(
39 delegate: ExtensionVersionSelectorDelegate,
40 window: &mut Window,
41 cx: &mut Context<Self>,
42 ) -> Self {
43 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
44 Self { picker }
45 }
46}
47
48pub struct ExtensionVersionSelectorDelegate {
49 fs: Arc<dyn Fs>,
50 selector: WeakEntity<ExtensionVersionSelector>,
51 extension_versions: Vec<ExtensionMetadata>,
52 selected_index: usize,
53 matches: Vec<StringMatch>,
54}
55
56impl ExtensionVersionSelectorDelegate {
57 pub fn new(
58 fs: Arc<dyn Fs>,
59 selector: WeakEntity<ExtensionVersionSelector>,
60 mut extension_versions: Vec<ExtensionMetadata>,
61 ) -> Self {
62 extension_versions.sort_unstable_by(|a, b| {
63 let a_version = SemanticVersion::from_str(&a.manifest.version);
64 let b_version = SemanticVersion::from_str(&b.manifest.version);
65
66 match (a_version, b_version) {
67 (Ok(a_version), Ok(b_version)) => b_version.cmp(&a_version),
68 _ => b.published_at.cmp(&a.published_at),
69 }
70 });
71
72 let matches = extension_versions
73 .iter()
74 .map(|extension| StringMatch {
75 candidate_id: 0,
76 score: 0.0,
77 positions: Default::default(),
78 string: format!("v{}", extension.manifest.version),
79 })
80 .collect();
81
82 Self {
83 fs,
84 selector,
85 extension_versions,
86 selected_index: 0,
87 matches,
88 }
89 }
90}
91
92impl PickerDelegate for ExtensionVersionSelectorDelegate {
93 type ListItem = ui::ListItem;
94
95 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
96 "Select extension version...".into()
97 }
98
99 fn match_count(&self) -> usize {
100 self.matches.len()
101 }
102
103 fn selected_index(&self) -> usize {
104 self.selected_index
105 }
106
107 fn set_selected_index(
108 &mut self,
109 ix: usize,
110 _window: &mut Window,
111 _cx: &mut Context<Picker<Self>>,
112 ) {
113 self.selected_index = ix;
114 }
115
116 fn update_matches(
117 &mut self,
118 query: String,
119 window: &mut Window,
120 cx: &mut Context<Picker<Self>>,
121 ) -> Task<()> {
122 let background_executor = cx.background_executor().clone();
123 let candidates = self
124 .extension_versions
125 .iter()
126 .enumerate()
127 .map(|(id, extension)| {
128 StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version))
129 })
130 .collect::<Vec<_>>();
131
132 cx.spawn_in(window, async move |this, cx| {
133 let matches = if query.is_empty() {
134 candidates
135 .into_iter()
136 .enumerate()
137 .map(|(index, candidate)| StringMatch {
138 candidate_id: index,
139 string: candidate.string,
140 positions: Vec::new(),
141 score: 0.0,
142 })
143 .collect()
144 } else {
145 match_strings(
146 &candidates,
147 &query,
148 false,
149 100,
150 &Default::default(),
151 background_executor,
152 )
153 .await
154 };
155
156 this.update(cx, |this, _cx| {
157 this.delegate.matches = matches;
158 this.delegate.selected_index = this
159 .delegate
160 .selected_index
161 .min(this.delegate.matches.len().saturating_sub(1));
162 })
163 .log_err();
164 })
165 }
166
167 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
168 if self.matches.is_empty() {
169 self.dismissed(window, cx);
170 return;
171 }
172
173 let candidate_id = self.matches[self.selected_index].candidate_id;
174 let extension_version = &self.extension_versions[candidate_id];
175
176 if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) {
177 return;
178 }
179
180 let extension_store = ExtensionStore::global(cx);
181 extension_store.update(cx, |store, cx| {
182 let extension_id = extension_version.id.clone();
183 let version = extension_version.manifest.version.clone();
184
185 update_settings_file::<ExtensionSettings>(self.fs.clone(), cx, {
186 let extension_id = extension_id.clone();
187 move |settings, _| {
188 settings.auto_update_extensions.insert(extension_id, false);
189 }
190 });
191
192 store.install_extension(extension_id, version, cx);
193 });
194 }
195
196 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
197 self.selector
198 .update(cx, |_, cx| cx.emit(DismissEvent))
199 .log_err();
200 }
201
202 fn render_match(
203 &self,
204 ix: usize,
205 selected: bool,
206 _: &mut Window,
207 cx: &mut Context<Picker<Self>>,
208 ) -> Option<Self::ListItem> {
209 let version_match = &self.matches[ix];
210 let extension_version = &self.extension_versions[version_match.candidate_id];
211
212 let is_version_compatible =
213 extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version);
214 let disabled = !is_version_compatible;
215
216 Some(
217 ListItem::new(ix)
218 .inset(true)
219 .spacing(ListItemSpacing::Sparse)
220 .toggle_state(selected)
221 .disabled(disabled)
222 .child(
223 HighlightedLabel::new(
224 version_match.string.clone(),
225 version_match.positions.clone(),
226 )
227 .when(disabled, |label| label.color(Color::Muted)),
228 )
229 .end_slot(
230 h_flex()
231 .gap_2()
232 .when(!is_version_compatible, |this| {
233 this.child(Label::new("Incompatible").color(Color::Muted))
234 })
235 .child(
236 Label::new(
237 extension_version
238 .published_at
239 .format("%Y-%m-%d")
240 .to_string(),
241 )
242 .when(disabled, |label| label.color(Color::Muted)),
243 ),
244 ),
245 )
246 }
247}