Add auto-completion support for snippet files (#23698)

loczek created

Release Notes:

- Added auto-completion support for snippet files.


![image](https://github.com/user-attachments/assets/ad165fc7-a6e7-426c-8892-f7004515dfc7)

Change summary

Cargo.lock                            |  2 +
crates/languages/Cargo.toml           |  1 
crates/languages/src/json.rs          | 12 +++++++
crates/paths/src/paths.rs             |  6 +++
crates/snippet_provider/Cargo.toml    |  1 
crates/snippet_provider/src/format.rs | 45 +++++++++++++++++++++++++++-
crates/snippet_provider/src/lib.rs    |  2 
7 files changed, 64 insertions(+), 5 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6956,6 +6956,7 @@ dependencies = [
  "serde_json",
  "settings",
  "smol",
+ "snippet_provider",
  "task",
  "text",
  "theme",
@@ -12059,6 +12060,7 @@ dependencies = [
  "gpui",
  "parking_lot",
  "paths",
+ "schemars",
  "serde",
  "serde_json",
  "snippet",

crates/languages/Cargo.toml 🔗

@@ -60,6 +60,7 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
+snippet_provider.workspace = true
 task.workspace = true
 toml.workspace = true
 tree-sitter = { workspace = true, optional = true }

crates/languages/src/json.rs 🔗

@@ -85,6 +85,7 @@ impl JsonLspAdapter {
             cx,
         );
         let tasks_schema = task::TaskTemplates::generate_json_schema();
+        let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();
         let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
         let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
 
@@ -125,8 +126,17 @@ impl JsonLspAdapter {
                             paths::local_tasks_file_relative_path()
                         ],
                         "schema": tasks_schema,
+                    },
+                    {
+                        "fileMatch": [
+                            schema_file_match(
+                                paths::snippets_dir()
+                                    .join("*.json")
+                                    .as_path()
+                            )
+                        ],
+                        "schema": snippets_schema,
                     }
-
                 ]
             }
         })

crates/paths/src/paths.rs 🔗

@@ -189,6 +189,12 @@ pub fn themes_dir() -> &'static PathBuf {
     THEMES_DIR.get_or_init(|| config_dir().join("themes"))
 }
 
+/// Returns the path to the snippets directory.
+pub fn snippets_dir() -> &'static PathBuf {
+    static SNIPPETS_DIR: OnceLock<PathBuf> = OnceLock::new();
+    SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets"))
+}
+
 /// Returns the path to the contexts directory.
 ///
 /// This is where the saved contexts from the Assistant are stored.

crates/snippet_provider/src/format.rs 🔗

@@ -1,13 +1,47 @@
 use collections::HashMap;
+use schemars::{
+    gen::SchemaSettings,
+    schema::{ObjectValidation, Schema, SchemaObject},
+    JsonSchema,
+};
 use serde::Deserialize;
+use serde_json::Value;
 
 #[derive(Deserialize)]
-pub(crate) struct VSSnippetsFile {
+pub struct VSSnippetsFile {
     #[serde(flatten)]
     pub(crate) snippets: HashMap<String, VSCodeSnippet>,
 }
 
-#[derive(Deserialize)]
+impl VSSnippetsFile {
+    pub fn generate_json_schema() -> Value {
+        let schema = SchemaSettings::draft07()
+            .with(|settings| settings.option_add_null_type = false)
+            .into_generator()
+            .into_root_schema_for::<Self>();
+
+        serde_json::to_value(schema).unwrap()
+    }
+}
+
+impl JsonSchema for VSSnippetsFile {
+    fn schema_name() -> String {
+        "VSSnippetsFile".into()
+    }
+
+    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
+        SchemaObject {
+            object: Some(Box::new(ObjectValidation {
+                additional_properties: Some(Box::new(gen.subschema_for::<VSCodeSnippet>())),
+                ..Default::default()
+            })),
+            ..Default::default()
+        }
+        .into()
+    }
+}
+
+#[derive(Deserialize, JsonSchema)]
 #[serde(untagged)]
 pub(crate) enum ListOrDirect {
     Single(String),
@@ -36,9 +70,14 @@ impl std::fmt::Display for ListOrDirect {
     }
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, JsonSchema)]
 pub(crate) struct VSCodeSnippet {
+    /// The snippet prefix used to decide whether a completion menu should be shown.
     pub(crate) prefix: Option<ListOrDirect>,
+
+    /// The snippet content. Use `$1` and `${1:defaultText}` to define cursor positions and `$0` for final cursor position.
     pub(crate) body: ListOrDirect,
+
+    /// The snippet description displayed inside the completion menu.
     pub(crate) description: Option<ListOrDirect>,
 }