extension_version_selector.rs

  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}