1mod format;
2mod registry;
3
4use std::{
5 path::{Path, PathBuf},
6 sync::Arc,
7 time::Duration,
8};
9
10use anyhow::Result;
11use collections::{BTreeMap, BTreeSet, HashMap};
12use format::VSSnippetsFile;
13use fs::Fs;
14use futures::stream::StreamExt;
15use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
16pub use registry::*;
17use util::ResultExt;
18
19pub fn init(cx: &mut AppContext) {
20 SnippetRegistry::init_global(cx);
21}
22
23// Is `None` if the snippet file is global.
24type SnippetKind = Option<String>;
25fn file_stem_to_key(stem: &str) -> SnippetKind {
26 if stem == "snippets" {
27 None
28 } else {
29 Some(stem.to_owned())
30 }
31}
32
33fn file_to_snippets(file_contents: VSSnippetsFile) -> Vec<Arc<Snippet>> {
34 let mut snippets = vec![];
35 for (prefix, snippet) in file_contents.snippets {
36 let prefixes = snippet
37 .prefix
38 .map_or_else(move || vec![prefix], |prefixes| prefixes.into());
39 let description = snippet
40 .description
41 .map(|description| description.to_string());
42 let body = snippet.body.to_string();
43 if snippet::Snippet::parse(&body).log_err().is_none() {
44 continue;
45 };
46 snippets.push(Arc::new(Snippet {
47 body,
48 prefix: prefixes,
49 description,
50 }));
51 }
52 snippets
53}
54// Snippet with all of the metadata
55#[derive(Debug)]
56pub struct Snippet {
57 pub prefix: Vec<String>,
58 pub body: String,
59 pub description: Option<String>,
60}
61
62async fn process_updates(
63 this: WeakModel<SnippetProvider>,
64 entries: Vec<PathBuf>,
65 mut cx: AsyncAppContext,
66) -> Result<()> {
67 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
68 for entry_path in entries {
69 if !entry_path
70 .extension()
71 .map_or(false, |extension| extension == "json")
72 {
73 continue;
74 }
75 let entry_metadata = fs.metadata(&entry_path).await;
76 // Entry could have been removed, in which case we should no longer show completions for it.
77 let entry_exists = entry_metadata.is_ok();
78 if entry_metadata.map_or(false, |entry| entry.map_or(false, |e| e.is_dir)) {
79 // Don't process dirs.
80 continue;
81 }
82 let Some(stem) = entry_path.file_stem().and_then(|s| s.to_str()) else {
83 continue;
84 };
85 let key = file_stem_to_key(stem);
86
87 let contents = if entry_exists {
88 fs.load(&entry_path).await.ok()
89 } else {
90 None
91 };
92
93 this.update(&mut cx, move |this, _| {
94 let snippets_of_kind = this.snippets.entry(key).or_default();
95 if entry_exists {
96 let Some(file_contents) = contents else {
97 return;
98 };
99 let Ok(as_json) = serde_json::from_str::<VSSnippetsFile>(&file_contents) else {
100 return;
101 };
102 let snippets = file_to_snippets(as_json);
103 *snippets_of_kind.entry(entry_path).or_default() = snippets;
104 } else {
105 snippets_of_kind.remove(&entry_path);
106 }
107 })?;
108 }
109 Ok(())
110}
111
112async fn initial_scan(
113 this: WeakModel<SnippetProvider>,
114 path: Arc<Path>,
115 mut cx: AsyncAppContext,
116) -> Result<()> {
117 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
118 let entries = fs.read_dir(&path).await;
119 if let Ok(entries) = entries {
120 let entries = entries
121 .collect::<Vec<_>>()
122 .await
123 .into_iter()
124 .collect::<Result<Vec<_>>>()?;
125 process_updates(this, entries, cx).await?;
126 }
127 Ok(())
128}
129
130pub struct SnippetProvider {
131 fs: Arc<dyn Fs>,
132 snippets: HashMap<SnippetKind, BTreeMap<PathBuf, Vec<Arc<Snippet>>>>,
133 watch_tasks: Vec<Task<Result<()>>>,
134}
135
136// Watches global snippet directory, is created just once and reused across multiple projects
137struct GlobalSnippetWatcher(Model<SnippetProvider>);
138
139impl GlobalSnippetWatcher {
140 fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Self {
141 let global_snippets_dir = paths::config_dir().join("snippets");
142 let provider = cx.new_model(|_cx| SnippetProvider {
143 fs,
144 snippets: Default::default(),
145 watch_tasks: vec![],
146 });
147 provider.update(cx, |this, cx| {
148 this.watch_directory(&global_snippets_dir, cx)
149 });
150 Self(provider)
151 }
152}
153
154impl gpui::Global for GlobalSnippetWatcher {}
155
156impl SnippetProvider {
157 pub fn new(
158 fs: Arc<dyn Fs>,
159 dirs_to_watch: BTreeSet<PathBuf>,
160 cx: &mut AppContext,
161 ) -> Model<Self> {
162 cx.new_model(move |cx| {
163 if !cx.has_global::<GlobalSnippetWatcher>() {
164 let global_watcher = GlobalSnippetWatcher::new(fs.clone(), cx);
165 cx.set_global(global_watcher);
166 }
167 let mut this = Self {
168 fs,
169 watch_tasks: Vec::new(),
170 snippets: Default::default(),
171 };
172
173 for dir in dirs_to_watch {
174 this.watch_directory(&dir, cx);
175 }
176
177 this
178 })
179 }
180
181 /// Add directory to be watched for content changes
182 fn watch_directory(&mut self, path: &Path, cx: &ModelContext<Self>) {
183 let path: Arc<Path> = Arc::from(path);
184
185 self.watch_tasks.push(cx.spawn(|this, mut cx| async move {
186 let fs = this.update(&mut cx, |this, _| this.fs.clone())?;
187 let watched_path = path.clone();
188 let watcher = fs.watch(&watched_path, Duration::from_secs(1));
189 initial_scan(this.clone(), path, cx.clone()).await?;
190
191 let (mut entries, _) = watcher.await;
192 while let Some(entries) = entries.next().await {
193 process_updates(
194 this.clone(),
195 entries.into_iter().map(|event| event.path).collect(),
196 cx.clone(),
197 )
198 .await?;
199 }
200 Ok(())
201 }));
202 }
203
204 fn lookup_snippets<'a, const LOOKUP_GLOBALS: bool>(
205 &'a self,
206 language: &'a SnippetKind,
207 cx: &AppContext,
208 ) -> Vec<Arc<Snippet>> {
209 let mut user_snippets: Vec<_> = self
210 .snippets
211 .get(language)
212 .cloned()
213 .unwrap_or_default()
214 .into_iter()
215 .flat_map(|(_, snippets)| snippets.into_iter())
216 .collect();
217 if LOOKUP_GLOBALS {
218 if let Some(global_watcher) = cx.try_global::<GlobalSnippetWatcher>() {
219 user_snippets.extend(
220 global_watcher
221 .0
222 .read(cx)
223 .lookup_snippets::<false>(language, cx),
224 );
225 }
226 }
227
228 let Some(registry) = SnippetRegistry::try_global(cx) else {
229 return user_snippets;
230 };
231
232 let registry_snippets = registry.get_snippets(language);
233 user_snippets.extend(registry_snippets);
234
235 user_snippets
236 }
237
238 pub fn snippets_for(&self, language: SnippetKind, cx: &AppContext) -> Vec<Arc<Snippet>> {
239 let mut requested_snippets = self.lookup_snippets::<true>(&language, cx);
240
241 if language.is_some() {
242 // Look up global snippets as well.
243 requested_snippets.extend(self.lookup_snippets::<true>(&None, cx));
244 }
245 requested_snippets
246 }
247}