extension_version_selector.rs

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