//! Functions for generating docs for our stdlib functions. use std::path::Path; use anyhow::Result; use convert_case::Casing; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tower_lsp::lsp_types::{ CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, InsertTextFormat, MarkupContent, MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation, }; use crate::std::Primitive; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct StdLibFnData { /// The name of the function. pub name: String, /// The summary of the function. pub summary: String, /// The description of the function. pub description: String, /// The tags of the function. pub tags: Vec, /// The args of the function. pub args: Vec, /// The return value of the function. pub return_value: Option, /// If the function is unpublished. pub unpublished: bool, /// If the function is deprecated. pub deprecated: bool, /// Code examples. /// These are tested and we know they compile and execute. pub examples: Vec, } /// This struct defines a single argument to a stdlib function. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] // There's a bug in ts_rs where this isn't correctly imported by StdLibFnData. #[ts(export_to = "StdLibFnData.ts")] #[serde(rename_all = "camelCase")] pub struct StdLibFnArg { /// The name of the argument. pub name: String, /// The type of the argument. pub type_: String, /// The schema of the argument. #[ts(type = "any")] pub schema: schemars::schema::Schema, /// If the argument is required. pub required: bool, } impl StdLibFnArg { #[allow(dead_code)] pub fn get_type_string(&self) -> Result<(String, bool)> { match get_type_string_from_schema(&self.schema.clone()) { Ok(r) => Ok(r), Err(e) => anyhow::bail!("error getting type string for {}: {:#?}", self.type_, e), } } pub fn get_autocomplete_string(&self) -> Result { get_autocomplete_string_from_schema(&self.schema.clone()) } pub fn get_autocomplete_snippet(&self, index: usize) -> Result> { if self.type_ == "SketchGroup" || self.type_ == "SketchGroupSet" || self.type_ == "ExtrudeGroup" || self.type_ == "ExtrudeGroupSet" || self.type_ == "SketchSurface" || self.type_ == "SketchSurfaceOrGroup" { return Ok(Some((index, format!("${{{}:{}}}", index, "%")))); } else if self.type_ == "TagDeclarator" && self.required { return Ok(Some((index, format!("${{{}:{}}}", index, "$myTag")))); } else if self.type_ == "TagIdentifier" && self.required { // TODO: actually use the ast to populate this. return Ok(Some((index, format!("${{{}:{}}}", index, "myTag")))); } get_autocomplete_snippet_from_schema(&self.schema.clone(), index) } pub fn description(&self) -> Option { get_description_string_from_schema(&self.schema.clone()) } } impl From for ParameterInformation { fn from(arg: StdLibFnArg) -> Self { ParameterInformation { label: ParameterLabel::Simple(arg.name.to_string()), documentation: arg.description().map(|description| { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: description, }) }), } } } /// This trait defines functions called upon stdlib functions to generate /// documentation for them. pub trait StdLibFn: std::fmt::Debug + Send + Sync { /// The name of the function. fn name(&self) -> String; /// The summary of the function. fn summary(&self) -> String; /// The description of the function. fn description(&self) -> String; /// The tags of the function. fn tags(&self) -> Vec; /// The args of the function. fn args(&self) -> Vec; /// The return value of the function. fn return_value(&self) -> Option; /// If the function is unpublished. fn unpublished(&self) -> bool; /// If the function is deprecated. fn deprecated(&self) -> bool; /// Any example code blocks. fn examples(&self) -> Vec; /// The function itself. fn std_lib_fn(&self) -> crate::std::StdFn; /// Helper function to clone the boxed trait object. fn clone_box(&self) -> Box; /// Return a JSON struct representing the function. fn to_json(&self) -> Result { Ok(StdLibFnData { name: self.name(), summary: self.summary(), description: self.description(), tags: self.tags(), args: self.args(), return_value: self.return_value(), unpublished: self.unpublished(), deprecated: self.deprecated(), examples: self.examples(), }) } fn fn_signature(&self) -> String { let mut signature = String::new(); signature.push_str(&format!("{}(", self.name())); for (i, arg) in self.args().iter().enumerate() { if i > 0 { signature.push_str(", "); } if arg.required { signature.push_str(&format!("{}: {}", arg.name, arg.type_)); } else { signature.push_str(&format!("{}?: {}", arg.name, arg.type_)); } } signature.push(')'); if let Some(return_value) = self.return_value() { signature.push_str(&format!(" -> {}", return_value.type_)); } signature } fn to_completion_item(&self) -> Result { Ok(CompletionItem { label: self.name(), label_details: Some(CompletionItemLabelDetails { detail: Some(self.fn_signature().replace(&self.name(), "")), description: None, }), kind: Some(CompletionItemKind::FUNCTION), detail: None, documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: if !self.description().is_empty() { format!("{}\n\n{}", self.summary(), self.description()) } else { self.summary() }, })), deprecated: Some(self.deprecated()), preselect: None, sort_text: None, filter_text: None, insert_text: Some(self.to_autocomplete_snippet()?), insert_text_format: Some(InsertTextFormat::SNIPPET), insert_text_mode: None, text_edit: None, additional_text_edits: None, command: None, commit_characters: None, data: None, tags: None, }) } fn to_autocomplete_snippet(&self) -> Result { let mut args = Vec::new(); let mut index = 0; for arg in self.args().iter() { if let Some((i, arg_str)) = arg.get_autocomplete_snippet(index)? { index = i + 1; args.push(arg_str); } } // We end with ${} so you can jump to the end of the snippet. // After the last argument. Ok(format!("{}({})${{}}", self.name(), args.join(", "))) } fn to_signature_help(&self) -> SignatureHelp { // Fill this in based on the current position of the cursor. let active_parameter = None; SignatureHelp { signatures: vec![SignatureInformation { label: self.name(), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: if !self.description().is_empty() { format!("{}\n\n{}", self.summary(), self.description()) } else { self.summary() }, })), parameters: Some(self.args().into_iter().map(|arg| arg.into()).collect()), active_parameter, }], active_signature: Some(0), active_parameter, } } } impl JsonSchema for dyn StdLibFn { fn schema_name() -> String { "StdLibFn".to_string() } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { gen.subschema_for::() } } impl Serialize for Box { fn serialize(&self, serializer: S) -> Result { self.to_json().unwrap().serialize(serializer) } } impl<'de> Deserialize<'de> for Box { fn deserialize>(deserializer: D) -> Result { let data = StdLibFnData::deserialize(deserializer)?; let stdlib = crate::std::StdLib::new(); let stdlib_fn = stdlib .get(&data.name) .ok_or_else(|| serde::de::Error::custom(format!("StdLibFn {} not found", data.name)))?; Ok(stdlib_fn) } } impl ts_rs::TS for dyn StdLibFn { type WithoutGenerics = Self; fn name() -> String { "StdLibFnData".to_string() } fn decl() -> String { StdLibFnData::decl() } fn decl_concrete() -> String { StdLibFnData::decl_concrete() } fn inline() -> String { StdLibFnData::inline() } fn inline_flattened() -> String { StdLibFnData::inline_flattened() } fn output_path() -> Option<&'static Path> { StdLibFnData::output_path() } } impl Clone for Box { fn clone(&self) -> Box { self.clone_box() } } pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option { if let schemars::schema::Schema::Object(o) = schema { if let Some(metadata) = &o.metadata { if let Some(description) = &metadata.description { return Some(description.to_string()); } } } None } pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<(String, bool)> { match schema { schemars::schema::Schema::Object(o) => { if let Some(enum_values) = &o.enum_values { let mut parsed_enum_values: Vec = Default::default(); let mut had_enum_string = false; for enum_value in enum_values { if let serde_json::value::Value::String(enum_value) = enum_value { had_enum_string = true; parsed_enum_values.push(format!("\"{}\"", enum_value)); } else { had_enum_string = false; break; } } if had_enum_string { return Ok((parsed_enum_values.join(" | "), false)); } } // Check if there if let Some(format) = &o.format { if format == "uuid" { return Ok((Primitive::Uuid.to_string(), false)); } else if format == "double" || format == "uint" || format == "int32" || format == "int64" || format == "uint8" || format == "uint32" || format == "uint64" { return Ok((Primitive::Number.to_string(), false)); } else { anyhow::bail!("unknown format: {}", format); } } if let Some(obj_val) = &o.object { let mut fn_docs = String::new(); fn_docs.push_str("{\n"); // Let's print out the object's properties. for (prop_name, prop) in obj_val.properties.iter() { if prop_name.starts_with('_') { continue; } // Make sure none of the object properties are in snake case. // We want the language to be consistent. // This will fail in the docs generation and not at runtime. if !prop_name.is_case(convert_case::Case::Camel) { anyhow::bail!("expected camel case: {:#?}", prop_name); } if let Some(description) = get_description_string_from_schema(prop) { fn_docs.push_str(&format!("\t// {}\n", description)); } fn_docs.push_str(&format!("\t{}: {},\n", prop_name, get_type_string_from_schema(prop)?.0,)); } fn_docs.push('}'); return Ok((fn_docs, true)); } if let Some(array_val) = &o.array { if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items { // Let's print out the object's properties. match array_val.max_items { Some(val) => { return Ok(( format!( "[{}]", (0..val) .map(|_| get_type_string_from_schema(items).unwrap().0) .collect::>() .join(", ") ), false, )); } None => { return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false)); } }; } else if let Some(items) = &array_val.contains { return Ok((format!("[{}]", get_type_string_from_schema(items)?.0), false)); } } if let Some(subschemas) = &o.subschemas { let mut fn_docs = String::new(); if let Some(items) = &subschemas.one_of { let mut had_enum_string = false; let mut parsed_enum_values: Vec = Vec::new(); for item in items { if let schemars::schema::Schema::Object(o) = item { if let Some(enum_values) = &o.enum_values { for enum_value in enum_values { if let serde_json::value::Value::String(enum_value) = enum_value { had_enum_string = true; parsed_enum_values.push(format!("\"{}\"", enum_value)); } else { had_enum_string = false; break; } } if !had_enum_string { break; } } else { had_enum_string = false; break; } } else { had_enum_string = false; break; } } if !had_enum_string { for (i, item) in items.iter().enumerate() { // Let's print out the object's properties. fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string()); if i < items.len() - 1 { fn_docs.push_str(" |\n"); } } } else { fn_docs.push_str(&parsed_enum_values.join(" | ")); } } else if let Some(items) = &subschemas.any_of { for (i, item) in items.iter().enumerate() { // Let's print out the object's properties. fn_docs.push_str(&get_type_string_from_schema(item)?.0.to_string()); if i < items.len() - 1 { fn_docs.push_str(" |\n"); } } } else { anyhow::bail!("unknown subschemas: {:#?}", subschemas); } return Ok((fn_docs, true)); } if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type { if schemars::schema::InstanceType::Boolean == **single { return Ok((Primitive::Bool.to_string(), false)); } else if schemars::schema::InstanceType::String == **single || schemars::schema::InstanceType::Null == **single { return Ok((Primitive::String.to_string(), false)); } } if let Some(reference) = &o.reference { return Ok((reference.replace("#/components/schemas/", ""), false)); } anyhow::bail!("unknown type: {:#?}", o) } schemars::schema::Schema::Bool(_) => Ok((Primitive::Bool.to_string(), false)), } } pub fn get_autocomplete_snippet_from_schema( schema: &schemars::schema::Schema, index: usize, ) -> Result> { match schema { schemars::schema::Schema::Object(o) => { if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") { if *nullable { return Ok(None); } } if o.enum_values.is_some() { let auto_str = get_autocomplete_string_from_schema(schema)?; return Ok(Some((index, format!("${{{}:{}}}", index, auto_str)))); } if let Some(format) = &o.format { if format == "uuid" { return Ok(Some((index, format!(r#"${{{}:"tag_or_edge_fn"}}"#, index)))); } else if format == "double" { return Ok(Some((index, format!(r#"${{{}:3.14}}"#, index)))); } else if format == "uint" || format == "int64" || format == "uint32" || format == "uint64" { return Ok(Some((index, format!(r#"${{{}:10}}"#, index)))); } else { anyhow::bail!("unknown format: {}", format); } } if let Some(obj_val) = &o.object { let mut fn_docs = String::new(); fn_docs.push_str("{\n"); // Let's print out the object's properties. let mut i = 0; for (prop_name, prop) in obj_val.properties.iter() { if prop_name.starts_with('_') { continue; } // Tolerance is a an optional property that we don't want to show in the // autocomplete, since it is mostly for advanced users. if prop_name == "tolerance" { continue; } if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(prop, index + i)? { fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet)); i += 1; } } fn_docs.push('}'); return Ok(Some((index + i - 1, fn_docs))); } if let Some(array_val) = &o.array { if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items { // Let's print out the object's properties. match array_val.max_items { Some(val) => { return Ok(Some(( index + (val as usize) - 1, format!( "[{}]", (0..val) .map(|v| get_autocomplete_snippet_from_schema(items, index + (v as usize)) .unwrap() .unwrap() .1) .collect::>() .join(", ") ), ))); } None => { return Ok(Some(( index, format!( "[{}]", get_autocomplete_snippet_from_schema(items, index)? .ok_or_else(|| anyhow::anyhow!("expected snippet"))? .1 ), ))); } }; } else if let Some(items) = &array_val.contains { return Ok(Some(( index, format!( "[{}]", get_autocomplete_snippet_from_schema(items, index)? .ok_or_else(|| anyhow::anyhow!("expected snippet"))? .1 ), ))); } } if let Some(subschemas) = &o.subschemas { let mut fn_docs = String::new(); if let Some(items) = &subschemas.one_of { let mut had_enum_string = false; let mut parsed_enum_values: Vec = Vec::new(); for item in items { if let schemars::schema::Schema::Object(o) = item { if let Some(enum_values) = &o.enum_values { for enum_value in enum_values { if let serde_json::value::Value::String(enum_value) = enum_value { had_enum_string = true; parsed_enum_values.push(format!("\"{}\"", enum_value)); } else { had_enum_string = false; break; } } if !had_enum_string { break; } } else { had_enum_string = false; break; } } else { had_enum_string = false; break; } } if had_enum_string && !parsed_enum_values.is_empty() { return Ok(Some((index, parsed_enum_values[0].to_string()))); } else if let Some(item) = items.iter().next() { if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(item, index)? { fn_docs.push_str(&snippet); } } } else if let Some(items) = &subschemas.any_of { if let Some(item) = items.iter().next() { if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(item, index)? { fn_docs.push_str(&snippet); } } } else { anyhow::bail!("unknown subschemas: {:#?}", subschemas); } return Ok(Some((index, fn_docs))); } if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type { if schemars::schema::InstanceType::Boolean == **single { return Ok(Some((index, format!(r#"${{{}:false}}"#, index)))); } else if schemars::schema::InstanceType::String == **single { return Ok(Some((index, format!(r#"${{{}:"string"}}"#, index)))); } else if schemars::schema::InstanceType::Null == **single { return Ok(None); } } anyhow::bail!("unknown type: {:#?}", o) } schemars::schema::Schema::Bool(_) => Ok(Some((index, format!(r#"${{{}:false}}"#, index)))), } } pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) -> Result { match schema { schemars::schema::Schema::Object(o) => { if let Some(enum_values) = &o.enum_values { let mut parsed_enum_values: Vec = Default::default(); let mut had_enum_string = false; for enum_value in enum_values { if let serde_json::value::Value::String(enum_value) = enum_value { had_enum_string = true; parsed_enum_values.push(format!("\"{}\"", enum_value)); } else { had_enum_string = false; break; } } if had_enum_string && !parsed_enum_values.is_empty() { return Ok(parsed_enum_values[0].to_string()); } } if let Some(format) = &o.format { if format == "uuid" { return Ok(Primitive::Uuid.to_string()); } else if format == "double" || format == "uint" || format == "int64" || format == "uint32" || format == "uint64" { return Ok(Primitive::Number.to_string()); } else { anyhow::bail!("unknown format: {}", format); } } if let Some(obj_val) = &o.object { let mut fn_docs = String::new(); fn_docs.push_str("{\n"); // Let's print out the object's properties. for (prop_name, prop) in obj_val.properties.iter() { if prop_name.starts_with('_') { continue; } fn_docs.push_str(&format!( "\t{}: {},\n", prop_name, get_autocomplete_string_from_schema(prop)?, )); } fn_docs.push('}'); return Ok(fn_docs); } if let Some(array_val) = &o.array { if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items { // Let's print out the object's properties. match array_val.max_items { Some(val) => { return Ok(format!( "[{}]", (0..val).map(|_| "number").collect::>().join(", ") )); } None => { return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?)); } }; } else if let Some(items) = &array_val.contains { return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?)); } } if let Some(subschemas) = &o.subschemas { let mut fn_docs = String::new(); if let Some(items) = &subschemas.one_of { let mut had_enum_string = false; let mut parsed_enum_values: Vec = Vec::new(); for item in items { if let schemars::schema::Schema::Object(o) = item { if let Some(enum_values) = &o.enum_values { for enum_value in enum_values { if let serde_json::value::Value::String(enum_value) = enum_value { had_enum_string = true; parsed_enum_values.push(format!("\"{}\"", enum_value)); } else { had_enum_string = false; break; } } if !had_enum_string { break; } } else { had_enum_string = false; break; } } else { had_enum_string = false; break; } } if had_enum_string && !parsed_enum_values.is_empty() { return Ok(parsed_enum_values[0].to_string()); } else if let Some(item) = items.iter().next() { // Let's print out the object's properties. fn_docs.push_str(&get_autocomplete_string_from_schema(item)?); } } else if let Some(items) = &subschemas.any_of { if let Some(item) = items.iter().next() { // Let's print out the object's properties. fn_docs.push_str(&get_autocomplete_string_from_schema(item)?); } } else { anyhow::bail!("unknown subschemas: {:#?}", subschemas); } return Ok(fn_docs); } if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type { if schemars::schema::InstanceType::Boolean == **single { return Ok(Primitive::Bool.to_string()); } else if schemars::schema::InstanceType::String == **single || schemars::schema::InstanceType::Null == **single { return Ok(Primitive::String.to_string()); } } anyhow::bail!("unknown type: {:#?}", o) } schemars::schema::Schema::Bool(_) => Ok(Primitive::Bool.to_string()), } } pub fn completion_item_from_enum_schema( schema: &schemars::schema::Schema, kind: CompletionItemKind, ) -> Result { // Get the docs for the schema. let description = get_description_string_from_schema(schema).unwrap_or_default(); let schemars::schema::Schema::Object(o) = schema else { anyhow::bail!("expected object schema: {:#?}", schema); }; let Some(enum_values) = o.enum_values.as_ref() else { anyhow::bail!("expected enum values: {:#?}", o); }; if enum_values.len() > 1 { anyhow::bail!("expected only one enum value: {:#?}", o); } if enum_values.is_empty() { anyhow::bail!("expected at least one enum value: {:#?}", o); } let serde_json::Value::String(ref enum_value) = enum_values[0] else { anyhow::bail!("expected string enum value: {:#?}", enum_values[0]); }; Ok(CompletionItem { label: enum_value.to_string(), label_details: None, kind: Some(kind), detail: Some(description.to_string()), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: description.to_string(), })), deprecated: Some(false), preselect: None, sort_text: None, filter_text: None, insert_text: None, insert_text_format: None, insert_text_mode: None, text_edit: None, additional_text_edits: None, command: None, commit_characters: None, data: None, tags: None, }) } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::StdLibFn; #[test] fn test_serialize_function() { let some_function = crate::ast::types::Function::StdLib { func: Box::new(crate::std::sketch::Line), }; let serialized = serde_json::to_string(&some_function).unwrap(); assert!(serialized.contains(r#"{"type":"StdLib""#)); } #[test] fn test_deserialize_function() { let some_function_string = r#"{"type":"StdLib","func":{"name":"line","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{}},"args":[],"unpublished":false,"deprecated":false, "examples": []}}"#; let some_function: crate::ast::types::Function = serde_json::from_str(some_function_string).unwrap(); assert_eq!( some_function, crate::ast::types::Function::StdLib { func: Box::new(crate::std::sketch::Line) } ); } #[test] fn get_autocomplete_snippet_line() { let line_fn: Box = Box::new(crate::std::sketch::Line); let snippet = line_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"line([${0:3.14}, ${1:3.14}], ${2:%})${}"#); } #[test] fn get_autocomplete_snippet_extrude() { let extrude_fn: Box = Box::new(crate::std::extrude::Extrude); let snippet = extrude_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"extrude(${0:3.14}, ${1:%})${}"#); } #[test] fn get_autocomplete_snippet_fillet() { let fillet_fn: Box = Box::new(crate::std::fillet::Fillet); let snippet = fillet_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"fillet({ radius: ${0:3.14}, tags: [${1:"tag_or_edge_fn"}], }, ${2:%})${}"# ); } #[test] fn get_autocomplete_snippet_start_sketch_on() { let start_sketch_on_fn: Box = Box::new(crate::std::sketch::StartSketchOn); let snippet = start_sketch_on_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"startSketchOn(${0:"XY"})${}"#); } #[test] fn get_autocomplete_snippet_pattern_circular_3d() { // We test this one specifically because it has ints and floats and strings. let pattern_fn: Box = Box::new(crate::std::patterns::PatternCircular3D); let snippet = pattern_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"patternCircular3d({ arcDegrees: ${0:3.14}, axis: [${1:3.14}, ${2:3.14}, ${3:3.14}], center: [${2:3.14}, ${3:3.14}, ${4:3.14}], repetitions: ${3:10}, rotateDuplicates: ${4:false}, }, ${5:%})${}"# ); } #[test] fn get_autocomplete_snippet_revolve() { let revolve_fn: Box = Box::new(crate::std::revolve::Revolve); let snippet = revolve_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"revolve({ axis: ${0:"X"}, }, ${1:%})${}"# ); } #[test] fn get_autocomplete_snippet_circle() { let circle_fn: Box = Box::new(crate::std::shapes::Circle); let snippet = circle_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"circle({ center: [${0:3.14}, ${1:3.14}], radius: ${1:3.14}, }, ${2:%})${}"# ); } }