snippets_ui.rs

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