1use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
2use gpui::{
3 App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled,
4 WeakEntity, Window, actions,
5};
6use language::LanguageRegistry;
7use paths::config_dir;
8use picker::{Picker, PickerDelegate};
9use std::{borrow::Borrow, fs, sync::Arc};
10use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
11use util::ResultExt;
12use workspace::{ModalView, OpenOptions, OpenVisible, Workspace, notifications::NotifyResultExt};
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, async move |_, cx| {
138 let scope = match scope_name.as_str() {
139 "Global" => "snippets".to_string(),
140 _ => language.await?.lsp_id(),
141 };
142
143 workspace.update_in(cx, |workspace, window, cx| {
144 workspace
145 .open_abs_path(
146 config_dir().join("snippets").join(scope + ".json"),
147 OpenOptions {
148 visible: Some(OpenVisible::None),
149 ..Default::default()
150 },
151 window,
152 cx,
153 )
154 .detach();
155 })
156 })
157 .detach_and_log_err(cx);
158 };
159 }
160 self.dismissed(window, cx);
161 }
162
163 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
164 self.scope_selector
165 .update(cx, |_, cx| cx.emit(DismissEvent))
166 .log_err();
167 }
168
169 fn selected_index(&self) -> usize {
170 self.selected_index
171 }
172
173 fn set_selected_index(
174 &mut self,
175 ix: usize,
176 _window: &mut Window,
177 _: &mut Context<Picker<Self>>,
178 ) {
179 self.selected_index = ix;
180 }
181
182 fn update_matches(
183 &mut self,
184 query: String,
185 window: &mut Window,
186 cx: &mut Context<Picker<Self>>,
187 ) -> gpui::Task<()> {
188 let background = cx.background_executor().clone();
189 let candidates = self.candidates.clone();
190 cx.spawn_in(window, async move |this, cx| {
191 let matches = if query.is_empty() {
192 candidates
193 .into_iter()
194 .enumerate()
195 .map(|(index, candidate)| StringMatch {
196 candidate_id: index,
197 string: candidate.string,
198 positions: Vec::new(),
199 score: 0.0,
200 })
201 .collect()
202 } else {
203 match_strings(
204 &candidates,
205 &query,
206 false,
207 100,
208 &Default::default(),
209 background,
210 )
211 .await
212 };
213
214 this.update(cx, |this, cx| {
215 let delegate = &mut this.delegate;
216 delegate.matches = matches;
217 delegate.selected_index = delegate
218 .selected_index
219 .min(delegate.matches.len().saturating_sub(1));
220 cx.notify();
221 })
222 .log_err();
223 })
224 }
225
226 fn render_match(
227 &self,
228 ix: usize,
229 selected: bool,
230 _window: &mut Window,
231 _: &mut Context<Picker<Self>>,
232 ) -> Option<Self::ListItem> {
233 let mat = &self.matches[ix];
234 let label = mat.string.clone();
235
236 Some(
237 ListItem::new(ix)
238 .inset(true)
239 .spacing(ListItemSpacing::Sparse)
240 .toggle_state(selected)
241 .child(HighlightedLabel::new(label, mat.positions.clone())),
242 )
243 }
244}