diff --git a/Cargo.lock b/Cargo.lock index 11a32c332f..2c22ee2e7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3932,6 +3932,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "snippet_provider", "task", "theme", "toml 0.8.10", @@ -9867,6 +9868,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", + "parking_lot", "serde", "serde_json", "snippet", @@ -13654,6 +13656,7 @@ dependencies = [ "settings", "simplelog", "smol", + "snippet_provider", "supermaven", "tab_switcher", "task", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0718205e00..73e97448f8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11767,7 +11767,7 @@ fn snippet_completions( let language = buffer.read(cx).language_at(buffer_position); let language_name = language.as_ref().map(|language| language.lsp_id()); let snippet_store = project.snippets().read(cx); - let snippets = snippet_store.snippets_for(language_name); + let snippets = snippet_store.snippets_for(language_name, cx); if snippets.is_empty() { return vec![]; diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index 4fada60527..8320681c73 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -42,6 +42,7 @@ semantic_version.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +snippet_provider.workspace = true theme.workspace = true toml.workspace = true ui.workspace = true diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 3c5357950d..d20b885eb2 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -526,6 +526,11 @@ fn populate_defaults(manifest: &mut ExtensionManifest, extension_path: &Path) -> } } + let snippets_json_path = extension_path.join("snippets.json"); + if snippets_json_path.exists() { + manifest.snippets = Some(snippets_json_path); + } + // For legacy extensions on the v0 schema (aka, using `extension.json`), we want to populate the grammars in // the manifest using the contents of the `grammars` directory. if manifest.schema_version.is_v0() { diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 797eec5a5e..24a2782c3d 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -78,6 +78,8 @@ pub struct ExtensionManifest { pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, + #[serde(default)] + pub snippets: Option, } #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] @@ -206,5 +208,6 @@ fn manifest_from_old_manifest( language_servers: Default::default(), slash_commands: BTreeMap::default(), indexed_docs_providers: BTreeMap::default(), + snippets: None, } } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index ccb679c230..1330b7ae6d 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -44,6 +44,7 @@ use release_channel::ReleaseChannel; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; +use snippet_provider::SnippetRegistry; use std::ops::RangeInclusive; use std::str::FromStr; use std::{ @@ -115,6 +116,7 @@ pub struct ExtensionStore { theme_registry: Arc, slash_command_registry: Arc, indexed_docs_registry: Arc, + snippet_registry: Arc, modified_extensions: HashSet>, wasm_host: Arc, wasm_extensions: Vec<(Arc, WasmExtension)>, @@ -193,6 +195,7 @@ pub fn init( theme_registry, SlashCommandRegistry::global(cx), IndexedDocsRegistry::global(cx), + SnippetRegistry::global(cx), cx, ) }); @@ -227,6 +230,7 @@ impl ExtensionStore { theme_registry: Arc, slash_command_registry: Arc, indexed_docs_registry: Arc, + snippet_registry: Arc, cx: &mut ModelContext, ) -> Self { let work_dir = extensions_dir.join("work"); @@ -259,6 +263,7 @@ impl ExtensionStore { theme_registry, slash_command_registry, indexed_docs_registry, + snippet_registry, reload_tx, tasks: Vec::new(), }; @@ -1045,6 +1050,7 @@ impl ExtensionStore { .collect::>(); let mut grammars_to_add = Vec::new(); let mut themes_to_add = Vec::new(); + let mut snippets_to_add = Vec::new(); for extension_id in &extensions_to_load { let Some(extension) = new_index.extensions.get(extension_id) else { continue; @@ -1062,6 +1068,11 @@ impl ExtensionStore { path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]); path })); + snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| { + let mut path = self.installed_dir.clone(); + path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]); + path + })); } self.language_registry @@ -1097,6 +1108,7 @@ impl ExtensionStore { let wasm_host = self.wasm_host.clone(); let root_dir = self.installed_dir.clone(); let theme_registry = self.theme_registry.clone(); + let snippet_registry = self.snippet_registry.clone(); let extension_entries = extensions_to_load .iter() .filter_map(|name| new_index.extensions.get(name).cloned()) @@ -1117,6 +1129,15 @@ impl ExtensionStore { .await .log_err(); } + + for snippets_path in &snippets_to_add { + if let Some(snippets_contents) = fs.load(snippets_path).await.log_err() + { + snippet_registry + .register_snippets(snippets_path, &snippets_contents) + .log_err(); + } + } } }) .await; diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index f2337aa6b9..d975e2ca90 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -19,6 +19,7 @@ use parking_lot::Mutex; use project::{Project, DEFAULT_COMPLETION_CONTEXT}; use serde_json::json; use settings::{Settings as _, SettingsStore}; +use snippet_provider::SnippetRegistry; use std::{ ffi::OsString, path::{Path, PathBuf}, @@ -160,6 +161,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), indexed_docs_providers: BTreeMap::default(), + snippets: None, }), dev: false, }, @@ -185,6 +187,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), indexed_docs_providers: BTreeMap::default(), + snippets: None, }), dev: false, }, @@ -258,6 +261,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); + let snippet_registry = Arc::new(SnippetRegistry::new()); let node_runtime = FakeNodeRuntime::new(); let store = cx.new_model(|cx| { @@ -272,6 +276,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { theme_registry.clone(), slash_command_registry.clone(), indexed_docs_registry.clone(), + snippet_registry.clone(), cx, ) }); @@ -345,6 +350,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { language_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), indexed_docs_providers: BTreeMap::default(), + snippets: None, }), dev: false, }, @@ -396,6 +402,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { theme_registry.clone(), slash_command_registry, indexed_docs_registry, + snippet_registry, cx, ) }); @@ -477,6 +484,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); + let snippet_registry = Arc::new(SnippetRegistry::new()); let node_runtime = FakeNodeRuntime::new(); let mut status_updates = language_registry.language_server_binary_statuses(); @@ -568,6 +576,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { theme_registry.clone(), slash_command_registry, indexed_docs_registry, + snippet_registry, cx, ) }); diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml index c6dd6fd9c0..75b7210a7a 100644 --- a/crates/snippet_provider/Cargo.toml +++ b/crates/snippet_provider/Cargo.toml @@ -14,6 +14,7 @@ collections.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true +parking_lot.workspace = true serde.workspace = true serde_json.workspace = true snippet.workspace = true diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index 7d0487a977..5e06254824 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -1,4 +1,5 @@ mod format; +mod registry; use std::{ path::{Path, PathBuf}, @@ -12,8 +13,13 @@ use format::VSSnippetsFile; use fs::Fs; use futures::stream::StreamExt; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel}; +pub use registry::*; use util::ResultExt; +pub fn init(cx: &mut AppContext) { + SnippetRegistry::init_global(cx); +} + // Is `None` if the snippet file is global. type SnippetKind = Option; fn file_stem_to_key(stem: &str) -> SnippetKind { @@ -168,28 +174,37 @@ impl SnippetProvider { Ok(()) }) } + fn lookup_snippets<'a>( &'a self, language: &'a SnippetKind, - ) -> Option> + 'a> { - Some( - self.snippets - .get(&language)? - .iter() - .flat_map(|(_, snippets)| snippets.iter().cloned()), - ) + cx: &AppContext, + ) -> Vec> { + let mut user_snippets: Vec<_> = self + .snippets + .get(&language) + .cloned() + .unwrap_or_default() + .into_iter() + .flat_map(|(_, snippets)| snippets.into_iter()) + .collect(); + + let Some(registry) = SnippetRegistry::try_global(cx) else { + return user_snippets; + }; + + let registry_snippets = registry.get_snippets(language); + user_snippets.extend(registry_snippets); + + user_snippets } - pub fn snippets_for(&self, language: SnippetKind) -> Vec> { - let mut requested_snippets: Vec<_> = self - .lookup_snippets(&language) - .map(|snippets| snippets.collect()) - .unwrap_or_default(); + pub fn snippets_for(&self, language: SnippetKind, cx: &AppContext) -> Vec> { + let mut requested_snippets = self.lookup_snippets(&language, cx); + if language.is_some() { // Look up global snippets as well. - if let Some(global_snippets) = self.lookup_snippets(&None) { - requested_snippets.extend(global_snippets); - } + requested_snippets.extend(self.lookup_snippets(&None, cx)); } requested_snippets } diff --git a/crates/snippet_provider/src/registry.rs b/crates/snippet_provider/src/registry.rs new file mode 100644 index 0000000000..86f0d6194e --- /dev/null +++ b/crates/snippet_provider/src/registry.rs @@ -0,0 +1,53 @@ +use std::{path::Path, sync::Arc}; + +use anyhow::Result; +use collections::HashMap; +use gpui::{AppContext, Global, ReadGlobal, UpdateGlobal}; +use parking_lot::RwLock; + +use crate::{file_stem_to_key, Snippet, SnippetKind}; + +struct GlobalSnippetRegistry(Arc); + +impl Global for GlobalSnippetRegistry {} + +#[derive(Default)] +pub struct SnippetRegistry { + snippets: RwLock>>>, +} + +impl SnippetRegistry { + pub fn global(cx: &AppContext) -> Arc { + GlobalSnippetRegistry::global(cx).0.clone() + } + + pub fn try_global(cx: &AppContext) -> Option> { + cx.try_global::() + .map(|registry| registry.0.clone()) + } + + pub fn init_global(cx: &mut AppContext) { + GlobalSnippetRegistry::set_global(cx, GlobalSnippetRegistry(Arc::new(Self::new()))) + } + + pub fn new() -> Self { + Self { + snippets: RwLock::new(HashMap::default()), + } + } + + pub fn register_snippets(&self, file_path: &Path, contents: &str) -> Result<()> { + let snippets_in_file: crate::format::VSSnippetsFile = serde_json::from_str(contents)?; + let kind = file_path + .file_stem() + .and_then(|stem| stem.to_str().and_then(file_stem_to_key)); + let snippets = crate::file_to_snippets(snippets_in_file); + self.snippets.write().insert(kind, snippets); + + Ok(()) + } + + pub fn get_snippets(&self, kind: &SnippetKind) -> Vec> { + self.snippets.read().get(kind).cloned().unwrap_or_default() + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index cd1a952c52..481dfb5017 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -87,6 +87,7 @@ serde_json.workspace = true settings.workspace = true simplelog = "0.9" smol.workspace = true +snippet_provider.workspace = true tab_switcher.workspace = true supermaven.workspace = true task.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3812e83c3c..5ee1997845 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -206,6 +206,7 @@ fn init_ui(app_state: Arc, cx: &mut AppContext) -> Result<()> { markdown_preview::init(cx); welcome::init(cx); extensions_ui::init(cx); + snippet_provider::init(cx); // Initialize each completion provider. Settings are used for toggling between them. let copilot_language_server_id = app_state.languages.next_language_server_id();