diff --git a/Cargo.lock b/Cargo.lock index 8e117caa22bc4640deb8f8b3b45c654e644d7954..349325e06856a3babf35a4cfd8f3bd2737fcc59d 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index b325e2250dce4cd6ce4368d17bddda6828611244..388a61e5145d552908ddf7e39fe59e17dfdd4b2a 100644 --- a/crates/languages/Cargo.toml +++ b/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 } diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 1ab00edbea06fb96fbc5c83e53b7154f1a15f5a2..81f4f479b2afeb624964a21e44fbe52c3ef50b39 100644 --- a/crates/languages/src/json.rs +++ b/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, } - ] } }) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 63b717b4e0f9639168ad3c5db13874d44cdbe09b..acb541aceabdab6d102158b34898b3d4e0d83502 100644 --- a/crates/paths/src/paths.rs +++ b/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 = 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. diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml index 69ea22c7b7a1974046e05d16c0ddac1b2a0913ce..6bdc4bbb07209f4cdefb15c8b8fac08729037988 100644 --- a/crates/snippet_provider/Cargo.toml +++ b/crates/snippet_provider/Cargo.toml @@ -21,3 +21,4 @@ serde.workspace = true serde_json.workspace = true snippet.workspace = true util.workspace = true +schemars.workspace = true diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs index b0b51cd32f58a45ddd8139706b55b71bc4480070..2a8acf0d654ff418fc6692d80e8f7430eb3b8192 100644 --- a/crates/snippet_provider/src/format.rs +++ b/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, } -#[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::(); + + 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::())), + ..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, + + /// 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, } diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index c2453f50198297932eafe97be4c92f79f3eaa174..d5f3de1b647ec7cf4ce1e962956d854f9e4aa584 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -1,5 +1,5 @@ mod extension_snippet; -mod format; +pub mod format; mod registry; use std::{