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