lib.rs

  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}