snippets_ui.rs

  1use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  2use gpui::{
  3    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Styled,
  4    View, ViewContext, VisualContext, WeakView,
  5};
  6use language::LanguageRegistry;
  7use paths::config_dir;
  8use picker::{Picker, PickerDelegate};
  9use std::{borrow::Borrow, fs, sync::Arc};
 10use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, WindowContext};
 11use util::ResultExt;
 12use workspace::{notifications::NotifyResultExt, ModalView, Workspace};
 13
 14actions!(snippets, [ConfigureSnippets, OpenFolder]);
 15
 16pub fn init(cx: &mut AppContext) {
 17    cx.observe_new_views(register).detach();
 18}
 19
 20fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 21    workspace.register_action(configure_snippets);
 22    workspace.register_action(open_folder);
 23}
 24
 25fn configure_snippets(
 26    workspace: &mut Workspace,
 27    _: &ConfigureSnippets,
 28    cx: &mut ViewContext<Workspace>,
 29) {
 30    let language_registry = workspace.app_state().languages.clone();
 31    let workspace_handle = workspace.weak_handle();
 32
 33    workspace.toggle_modal(cx, move |cx| {
 34        ScopeSelector::new(language_registry, workspace_handle, cx)
 35    });
 36}
 37
 38fn open_folder(workspace: &mut Workspace, _: &OpenFolder, cx: &mut ViewContext<Workspace>) {
 39    fs::create_dir_all(config_dir().join("snippets")).notify_err(workspace, cx);
 40    cx.open_with_system(config_dir().join("snippets").borrow());
 41}
 42
 43pub struct ScopeSelector {
 44    picker: View<Picker<ScopeSelectorDelegate>>,
 45}
 46
 47impl ScopeSelector {
 48    fn new(
 49        language_registry: Arc<LanguageRegistry>,
 50        workspace: WeakView<Workspace>,
 51        cx: &mut ViewContext<Self>,
 52    ) -> Self {
 53        let delegate =
 54            ScopeSelectorDelegate::new(workspace, cx.view().downgrade(), language_registry);
 55
 56        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
 57
 58        Self { picker }
 59    }
 60}
 61
 62impl ModalView for ScopeSelector {}
 63
 64impl EventEmitter<DismissEvent> for ScopeSelector {}
 65
 66impl FocusableView for ScopeSelector {
 67    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
 68        self.picker.focus_handle(cx)
 69    }
 70}
 71
 72impl Render for ScopeSelector {
 73    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
 74        v_flex().w(rems(34.)).child(self.picker.clone())
 75    }
 76}
 77
 78pub struct ScopeSelectorDelegate {
 79    workspace: WeakView<Workspace>,
 80    scope_selector: WeakView<ScopeSelector>,
 81    language_registry: Arc<LanguageRegistry>,
 82    candidates: Vec<StringMatchCandidate>,
 83    matches: Vec<StringMatch>,
 84    selected_index: usize,
 85}
 86
 87impl ScopeSelectorDelegate {
 88    fn new(
 89        workspace: WeakView<Workspace>,
 90        scope_selector: WeakView<ScopeSelector>,
 91        language_registry: Arc<LanguageRegistry>,
 92    ) -> Self {
 93        let candidates = Vec::from(["Global".to_string()]).into_iter();
 94        let languages = language_registry.language_names().into_iter();
 95
 96        let candidates = candidates
 97            .chain(languages)
 98            .enumerate()
 99            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
100            .collect::<Vec<_>>();
101
102        Self {
103            workspace,
104            scope_selector,
105            language_registry,
106            candidates,
107            matches: vec![],
108            selected_index: 0,
109        }
110    }
111}
112
113impl PickerDelegate for ScopeSelectorDelegate {
114    type ListItem = ListItem;
115
116    fn placeholder_text(&self, _: &mut WindowContext) -> Arc<str> {
117        "Select snippet scope...".into()
118    }
119
120    fn match_count(&self) -> usize {
121        self.matches.len()
122    }
123
124    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
125        if let Some(mat) = self.matches.get(self.selected_index) {
126            let scope_name = self.candidates[mat.candidate_id].string.clone();
127            let language = self.language_registry.language_for_name(&scope_name);
128
129            if let Some(workspace) = self.workspace.upgrade() {
130                cx.spawn(|_, mut cx| async move {
131                    let scope = match scope_name.as_str() {
132                        "Global" => "snippets".to_string(),
133                        _ => language.await?.lsp_id(),
134                    };
135
136                    workspace.update(&mut cx, |workspace, cx| {
137                        workspace
138                            .open_abs_path(
139                                config_dir().join("snippets").join(scope + ".json"),
140                                false,
141                                cx,
142                            )
143                            .detach();
144                    })
145                })
146                .detach_and_log_err(cx);
147            };
148        }
149        self.dismissed(cx);
150    }
151
152    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
153        self.scope_selector
154            .update(cx, |_, cx| cx.emit(DismissEvent))
155            .log_err();
156    }
157
158    fn selected_index(&self) -> usize {
159        self.selected_index
160    }
161
162    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
163        self.selected_index = ix;
164    }
165
166    fn update_matches(
167        &mut self,
168        query: String,
169        cx: &mut ViewContext<Picker<Self>>,
170    ) -> gpui::Task<()> {
171        let background = cx.background_executor().clone();
172        let candidates = self.candidates.clone();
173        cx.spawn(|this, mut cx| async move {
174            let matches = if query.is_empty() {
175                candidates
176                    .into_iter()
177                    .enumerate()
178                    .map(|(index, candidate)| StringMatch {
179                        candidate_id: index,
180                        string: candidate.string,
181                        positions: Vec::new(),
182                        score: 0.0,
183                    })
184                    .collect()
185            } else {
186                match_strings(
187                    &candidates,
188                    &query,
189                    false,
190                    100,
191                    &Default::default(),
192                    background,
193                )
194                .await
195            };
196
197            this.update(&mut cx, |this, cx| {
198                let delegate = &mut this.delegate;
199                delegate.matches = matches;
200                delegate.selected_index = delegate
201                    .selected_index
202                    .min(delegate.matches.len().saturating_sub(1));
203                cx.notify();
204            })
205            .log_err();
206        })
207    }
208
209    fn render_match(
210        &self,
211        ix: usize,
212        selected: bool,
213        _: &mut ViewContext<Picker<Self>>,
214    ) -> Option<Self::ListItem> {
215        let mat = &self.matches[ix];
216        let label = mat.string.clone();
217
218        Some(
219            ListItem::new(ix)
220                .inset(true)
221                .spacing(ListItemSpacing::Sparse)
222                .toggle_state(selected)
223                .child(HighlightedLabel::new(label, mat.positions.clone())),
224        )
225    }
226}