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 {
pub name: String,
pub summary: String,
pub description: String,
pub tags: Vec<String>,
pub args: Vec<StdLibFnArg>,
pub return_value: Option<StdLibFnArg>,
pub unpublished: bool,
pub deprecated: bool,
pub examples: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnArg {
pub name: String,
pub type_: String,
#[ts(type = "any")]
pub schema: schemars::schema::Schema,
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<String> {
get_autocomplete_string_from_schema(&self.schema.clone())
}
pub fn get_autocomplete_snippet(&self, index: usize) -> Result<Option<(usize, String)>> {
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 {
return Ok(Some((index, format!("${{{}:{}}}", index, "myTag"))));
}
get_autocomplete_snippet_from_schema(&self.schema.clone(), index)
}
pub fn description(&self) -> Option<String> {
get_description_string_from_schema(&self.schema.clone())
}
}
impl From<StdLibFnArg> 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,
})
}),
}
}
}
pub trait StdLibFn: std::fmt::Debug + Send + Sync {
fn name(&self) -> String;
fn summary(&self) -> String;
fn description(&self) -> String;
fn tags(&self) -> Vec<String>;
fn args(&self) -> Vec<StdLibFnArg>;
fn return_value(&self) -> Option<StdLibFnArg>;
fn unpublished(&self) -> bool;
fn deprecated(&self) -> bool;
fn examples(&self) -> Vec<String>;
fn std_lib_fn(&self) -> crate::std::StdFn;
fn clone_box(&self) -> Box<dyn StdLibFn>;
fn to_json(&self) -> Result<StdLibFnData> {
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<CompletionItem> {
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<String> {
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);
}
}
Ok(format!("{}({})${{}}", self.name(), args.join(", ")))
}
fn to_signature_help(&self) -> SignatureHelp {
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::<StdLibFnData>()
}
}
impl Serialize for Box<dyn StdLibFn> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.to_json().unwrap().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Box<dyn StdLibFn> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<dyn StdLibFn> {
fn clone(&self) -> Box<dyn StdLibFn> {
self.clone_box()
}
}
pub fn get_description_string_from_schema(schema: &schemars::schema::Schema) -> Option<String> {
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<String> = 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));
}
}
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");
for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') {
continue;
}
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 {
match array_val.max_items {
Some(val) => {
return Ok((
format!(
"[{}]",
(0..val)
.map(|_| get_type_string_from_schema(items).unwrap().0)
.collect::<Vec<_>>()
.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<String> = 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() {
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() {
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<Option<(usize, String)>> {
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 mut i = 0;
for (prop_name, prop) in obj_val.properties.iter() {
if prop_name.starts_with('_') {
continue;
}
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 {
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::<Vec<_>>()
.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<String> = 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<String> {
match schema {
schemars::schema::Schema::Object(o) => {
if let Some(enum_values) = &o.enum_values {
let mut parsed_enum_values: Vec<String> = 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");
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 {
match array_val.max_items {
Some(val) => {
return Ok(format!(
"[{}]",
(0..val).map(|_| "number").collect::<Vec<_>>().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<String> = 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() {
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() {
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<CompletionItem> {
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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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() {
let pattern_fn: Box<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = Box::new(crate::std::shapes::Circle);
let snippet = circle_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"circle([${0:3.14}, ${1:3.14}], ${2:3.14}, ${3:%})${}"#);
}
}