//! Functions for generating docs for our stdlib functions. #[cfg(test)] mod gen_std_tests; pub mod kcl_doc; use std::{ fmt::{self, Write}, path::Path, }; use anyhow::Result; use kcl_doc::ModData; use parse_display::Display; 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::execution::{types::NumericType, Sketch}; // These types are declared in (KCL) std. const DECLARED_TYPES: [&str; 17] = [ "any", "number", "string", "tag", "bool", "Sketch", "Solid", "Plane", "Helix", "Face", "Edge", "Point2d", "Point3d", "Axis2d", "Axis3d", "ImportedGeometry", "fn", ]; lazy_static::lazy_static! { static ref NUMERIC_TYPE_SCHEMA: schemars::schema::SchemaObject = { let mut settings = schemars::gen::SchemaSettings::openapi3(); settings.inline_subschemas = true; let mut generator = schemars::gen::SchemaGenerator::new(settings); generator.root_schema_for::().schema }; } /// The primitive types that can be used in a KCL file. #[derive(Debug, Clone, PartialEq, Serialize, JsonSchema, Display)] #[serde(rename_all = "lowercase")] #[display(style = "lowercase")] enum Primitive { /// A boolean value. Bool, /// A number value. Number, /// A string value. String, /// A uuid value. Uuid, } #[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, /// If this function uses keyword arguments, or positional arguments. pub keyword_arguments: bool, /// 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. The bool is whether the example is `norun`` /// These are tested and we know they compile and execute. pub examples: Vec<(String, bool)>, } /// 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::RootSchema, /// If the argument is required. pub required: bool, /// Include this in completion snippets? #[serde(default, skip_serializing_if = "is_false")] pub include_in_snippet: bool, /// Additional information that could be used instead of the type's description. /// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about /// how this argument is meant to be used. /// Empty string means this has no docs. #[serde(default, skip_serializing_if = "String::is_empty")] pub description: String, /// Even in functions that use keyword arguments, not every parameter requires a label (most do though). /// Some functions allow one unlabeled parameter, which has to be first in the /// argument list. /// /// This field is ignored for functions that still use positional arguments. /// Defaults to true. #[serde(default = "its_true")] pub label_required: bool, } impl fmt::Display for StdLibFnArg { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !self.label_required { f.write_char('@')?; } f.write_str(&self.name)?; if !self.required { f.write_char('?')?; } f.write_str(": ")?; f.write_str(&self.type_) } } fn its_true() -> bool { true } fn is_false(b: &bool) -> bool { !b } impl StdLibFnArg { /// If the argument is a primitive. pub fn is_primitive(&self) -> Result { is_primitive(&self.schema.schema.clone().into()).map(|r| r.is_some()) } pub fn get_autocomplete_string(&self) -> Result { get_autocomplete_string_from_schema(&self.schema.schema.clone().into()) } pub fn get_autocomplete_snippet(&self, index: usize, in_keyword_fn: bool) -> Result> { let label = if in_keyword_fn && self.label_required { &format!("{} = ", self.name) } else { "" }; if (self.type_ == "Sketch" || self.type_ == "[Sketch]" || self.type_ == "Geometry" || self.type_ == "GeometryWithImportedGeometry" || self.type_ == "Solid" || self.type_ == "[Solid]" || self.type_ == "SketchSurface" || self.type_ == "SketchOrSurface" || self.type_ == "SolidOrImportedGeometry" || self.type_ == "SolidOrSketchOrImportedGeometry") && (self.required || self.include_in_snippet) { return Ok(Some((index, format!("{label}${{{}:{}}}", index, "%")))); } else if (self.type_ == "TagDeclarator" || self.type_ == "TagNode") && self.required { return Ok(Some((index, format!("{label}${{{}:{}}}", index, "$myTag")))); } else if self.type_ == "TagIdentifier" && self.required { // TODO: actually use the ast to populate this. return Ok(Some((index, format!("{label}${{{}:{}}}", index, "myTag")))); } else if self.type_ == "[KclValue]" && self.required { return Ok(Some((index, format!("{label}${{{}:{}}}", index, "[0..9]")))); } else if self.type_ == "KclValue" && self.required { return Ok(Some((index, format!("{label}${{{}:{}}}", index, "3")))); } self.get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index, in_keyword_fn, &self.name) .map(|maybe| maybe.map(|(index, snippet)| (index, format!("{label}{snippet}")))) } pub fn description(&self, kcl_std: Option<&ModData>) -> Option { // Check if we explicitly gave this stdlib arg a description. if !self.description.is_empty() { assert!(!self.description.contains('\n'), "Arg docs will get truncated"); return Some(self.description.clone()); } if let Some(kcl_std) = kcl_std { if let Some(t) = docs_for_type(&self.type_, kcl_std) { return Some(t); } } // If not, then try to get something meaningful from the schema. get_description_string_from_schema(&self.schema.clone()) } fn get_autocomplete_snippet_from_schema( &self, schema: &schemars::schema::Schema, index: usize, in_keyword_fn: bool, name: &str, ) -> Result> { match schema { schemars::schema::Schema::Object(o) => { // Check if the schema is the same as a Sketch. let mut settings = schemars::gen::SchemaSettings::openapi3(); // We set this so we can recurse them later. settings.inline_subschemas = true; let mut generator = schemars::gen::SchemaGenerator::new(settings); let sketch_schema = generator.root_schema_for::().schema; if sketch_schema.object == o.object { return Ok(Some((index, format!("${{{}:sketch{}}}", index, "000")))); } if name == "color" { let snippet = format!("${{{}:\"#ff0000\"}}", index); return Ok(Some((index, snippet))); } if let Some(serde_json::Value::Bool(nullable)) = o.extensions.get("nullable") { if (!in_keyword_fn && *nullable) || (in_keyword_fn && !self.include_in_snippet) { 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" || format == "uint8" { 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 = index; 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((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(prop, i, false, name)? { fn_docs.push_str(&format!("\t{} = {},\n", prop_name, snippet)); i = new_index + 1; } } fn_docs.push('}'); return Ok(Some((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| self .get_autocomplete_snippet_from_schema( items, index + (v as usize), in_keyword_fn, name ) .unwrap() .unwrap() .1) .collect::>() .join(", ") ), ))); } None => { return Ok(Some(( index, format!( "[{}]", self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)? .ok_or_else(|| anyhow::anyhow!("expected snippet"))? .1 ), ))); } }; } else if let Some(items) = &array_val.contains { return Ok(Some(( index, format!( "[{}]", self.get_autocomplete_snippet_from_schema(items, index, in_keyword_fn, name)? .ok_or_else(|| anyhow::anyhow!("expected snippet"))? .1 ), ))); } } if let Some(subschemas) = &o.subschemas { let mut fn_docs = String::new(); let mut i = index; 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((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)? { i = new_index + 1; fn_docs.push_str(&snippet); } } } else if let Some(items) = &subschemas.any_of { if let Some(item) = items.iter().next() { if let Some((new_index, snippet)) = self.get_autocomplete_snippet_from_schema(item, index, in_keyword_fn, name)? { i = new_index + 1; fn_docs.push_str(&snippet); } } } else { anyhow::bail!("unknown subschemas: {:#?}", subschemas); } return Ok(Some((i - 1, 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)))), } } } impl From for ParameterInformation { fn from(arg: StdLibFnArg) -> Self { ParameterInformation { label: ParameterLabel::Simple(arg.name.to_string()), documentation: arg.description(None).map(|description| { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: description, }) }), } } } fn docs_for_type(ty: &str, kcl_std: &ModData) -> Option { let key = if ty.starts_with("number") { "number" } else { ty }; if DECLARED_TYPES.contains(&key) { if let Some(data) = kcl_std.find_by_name(key) { return data.summary().cloned(); } } None } /// 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; /// Does this use keyword arguments, or positional? fn keyword_arguments(&self) -> bool; /// The tags of the function. fn tags(&self) -> Vec; /// The args of the function. fn args(&self, inline_subschemas: bool) -> Vec; /// The return value of the function. fn return_value(&self, inline_subschemas: bool) -> Option; /// If the function is unpublished. fn unpublished(&self) -> bool; /// If the function is deprecated. fn deprecated(&self) -> bool; /// If the function should appear in the feature tree. fn feature_tree_operation(&self) -> bool; /// Any example code blocks. fn examples(&self) -> Vec<(String, bool)>; /// 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(), keyword_arguments: self.keyword_arguments(), args: self.args(false), return_value: self.return_value(false), unpublished: self.unpublished(), deprecated: self.deprecated(), examples: self.examples(), }) } fn fn_signature(&self, include_name: bool) -> String { let mut signature = String::new(); if include_name { signature.push_str(&self.name()); } let args = self.args(false); if args.is_empty() { signature.push_str("()"); } else if args.len() == 1 { signature.push('('); signature.push_str(&args[0].to_string()); signature.push(')'); } else { signature.push('('); for a in args { signature.push_str("\n "); signature.push_str(&a.to_string()); signature.push(','); } signature.push('\n'); signature.push(')'); } if let Some(return_value) = self.return_value(false) { 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(false)), 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 { if self.name() == "loft" { return Ok("loft([${0:sketch000}, ${1:sketch001}])".to_string()); } else if self.name() == "union" { return Ok("union([${0:extrude001}, ${1:extrude002}])".to_string()); } else if self.name() == "subtract" { return Ok("subtract([${0:extrude001}], tools = [${1:extrude002}])".to_string()); } else if self.name() == "intersect" { return Ok("intersect([${0:extrude001}, ${1:extrude002}])".to_string()); } else if self.name() == "subtract2D" { return Ok("subtract2d(${0:%}, tool = ${1:%})".to_string()); } let in_keyword_fn = self.keyword_arguments(); let mut args = Vec::new(); let mut index = 0; for arg in self.args(true).iter() { if let Some((i, arg_str)) = arg.get_autocomplete_snippet(index, in_keyword_fn)? { index = i + 1; args.push(arg_str); } } 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.fn_signature(true), documentation: Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: format!( r#"{} {}"#, self.summary(), self.description() ) .trim() .to_string(), })), parameters: Some(self.args(true).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::RootSchema) -> Option { if let Some(metadata) = &schema.schema.metadata { if let Some(description) = &metadata.description { return Some(description.to_string()); } } if let Some(reference) = &schema.schema.reference { if let Some(definition) = schema.definitions.get(reference.split('/').next_back().unwrap_or("")) { let schemars::schema::Schema::Object(definition) = definition else { return None; }; if let Some(metadata) = &definition.metadata { if let Some(description) = &metadata.description { return Some(description.to_string()); } } } } // If we have subschemas iterate over them and recursively create references. if let Some(subschema) = &schema.schema.subschemas { if let Some(one_of) = &subschema.one_of { if one_of.len() == 1 { return get_description_string_from_schema(&schemars::schema::RootSchema { meta_schema: schema.meta_schema.clone(), schema: one_of[0].clone().into(), definitions: schema.definitions.clone(), }); } } if let Some(all_of) = &subschema.all_of { if all_of.len() == 1 { return get_description_string_from_schema(&schemars::schema::RootSchema { meta_schema: schema.meta_schema.clone(), schema: all_of[0].clone().into(), definitions: schema.definitions.clone(), }); } } if let Some(any_of) = &subschema.any_of { if any_of.len() == 1 { return get_description_string_from_schema(&schemars::schema::RootSchema { meta_schema: schema.meta_schema.clone(), schema: any_of[0].clone().into(), definitions: schema.definitions.clone(), }); } } } None } fn is_primitive(schema: &schemars::schema::Schema) -> Result> { match schema { schemars::schema::Schema::Object(o) => { if o.enum_values.is_some() { // It's an enum so it's not a primitive. return Ok(None); } // Check if there if let Some(format) = &o.format { if format == "uuid" { return Ok(Some(Primitive::Uuid)); } else if format == "double" || format == "uint" || format == "int32" || format == "int64" || format == "uint8" || format == "uint32" || format == "uint64" { return Ok(Some(Primitive::Number)); } else { anyhow::bail!("unknown format: {}", format); } } if o.object.is_some() { // It's an object so it's not a primitive. return Ok(None); } if o.array.is_some() { return Ok(None); } if o.subschemas.is_some() { return Ok(None); } if let Some(schemars::schema::SingleOrVec::Single(single)) = &o.instance_type { if schemars::schema::InstanceType::Boolean == **single { return Ok(Some(Primitive::Bool)); } else if schemars::schema::InstanceType::String == **single || schemars::schema::InstanceType::Null == **single { return Ok(Some(Primitive::String)); } } if o.reference.is_some() { return Ok(None); } anyhow::bail!("unknown type: {:#?}", o) } schemars::schema::Schema::Bool(_) => Ok(Some(Primitive::Bool)), } } 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(enum_value.to_owned()); } 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 == "uint8" || 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()), } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::StdLibFn; use crate::docs::kcl_doc::{self, DocData}; #[test] fn test_serialize_function() { let some_function = crate::parsing::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","keywordArguments":false,"summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{},"schemaDefinitions":{}},"args":[],"unpublished":false,"deprecated":false, "examples": []}}"#; let some_function: crate::parsing::ast::types::Function = serde_json::from_str(some_function_string).unwrap(); assert_eq!( some_function, crate::parsing::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:%}, end = [${1:3.14}, ${2:3.14}])"#); } #[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:%}, length = ${1:3.14})"#); } #[test] fn get_autocomplete_snippet_fillet() { let data = kcl_doc::walk_prelude(); let DocData::Fn(fillet_fn) = data.find_by_name("fillet").unwrap() else { panic!(); }; let snippet = fillet_fn.to_autocomplete_snippet(); assert_eq!(snippet, r#"fillet(radius = ${0:3.14}, tags = [${1:tag_or_edge_fn}])"#); } #[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(${0:%}, instances = ${1:10}, axis = [${2:3.14}, ${3:3.14}, ${4:3.14}], center = [${5:3.14}, ${6:3.14}, ${7:3.14}], arcDegrees = ${8:3.14}, rotateDuplicates = ${9:false})"# ); } #[test] fn get_autocomplete_snippet_revolve() { let data = kcl_doc::walk_prelude(); let DocData::Fn(revolve_fn) = data.find_by_name("revolve").unwrap() else { panic!(); }; let snippet = revolve_fn.to_autocomplete_snippet(); assert_eq!(snippet, r#"revolve(axis = ${0:X})"#); } #[test] fn get_autocomplete_snippet_circle() { let data = kcl_doc::walk_prelude(); let DocData::Fn(circle_fn) = data.find_by_name("circle").unwrap() else { panic!(); }; let snippet = circle_fn.to_autocomplete_snippet(); assert_eq!( snippet, r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})"# ); } #[test] fn get_autocomplete_snippet_arc() { let arc_fn: Box = Box::new(crate::std::sketch::Arc); let snippet = arc_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"arc(${0:%}, angleStart = ${1:3.14}, angleEnd = ${2:3.14}, radius = ${3:3.14})"# ); } #[test] fn get_autocomplete_snippet_map() { let data = kcl_doc::walk_prelude(); let DocData::Fn(map_fn) = data.find_by_name("map").unwrap() else { panic!(); }; let snippet = map_fn.to_autocomplete_snippet(); assert_eq!(snippet, r#"map()"#); } #[test] fn get_autocomplete_snippet_pattern_linear_2d() { let pattern_fn: Box = Box::new(crate::std::patterns::PatternLinear2D); let snippet = pattern_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"patternLinear2d(${0:%}, instances = ${1:10}, distance = ${2:3.14}, axis = [${3:3.14}, ${4:3.14}])"# ); } #[test] fn get_autocomplete_snippet_appearance() { let appearance_fn: Box = Box::new(crate::std::appearance::Appearance); let snippet = appearance_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})"# ); } #[test] fn get_autocomplete_snippet_loft() { let loft_fn: Box = Box::new(crate::std::loft::Loft); let snippet = loft_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"loft([${0:sketch000}, ${1:sketch001}])"#); } #[test] fn get_autocomplete_snippet_sweep() { let sweep_fn: Box = Box::new(crate::std::sweep::Sweep); let snippet = sweep_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})"#); } #[test] fn get_autocomplete_snippet_hole() { let f: Box = Box::new(crate::std::sketch::Subtract2D); let snippet = f.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"subtract2d(${0:%}, tool = ${1:%})"#); } #[test] fn get_autocomplete_snippet_helix() { let data = kcl_doc::walk_prelude(); let DocData::Fn(helix_fn) = data.find_by_name("helix").unwrap() else { panic!(); }; let snippet = helix_fn.to_autocomplete_snippet(); assert_eq!( snippet, r#"helix(revolutions = ${0:3.14}, angleStart = ${1:3.14}, radius = ${2:3.14}, axis = ${3:X}, length = ${4:3.14})"# ); } #[test] fn get_autocomplete_snippet_union() { let union_fn: Box = Box::new(crate::std::csg::Union); let snippet = union_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"union([${0:extrude001}, ${1:extrude002}])"#); } #[test] fn get_autocomplete_snippet_subtract() { let subtract_fn: Box = Box::new(crate::std::csg::Subtract); let snippet = subtract_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"subtract([${0:extrude001}], tools = [${1:extrude002}])"#); } #[test] fn get_autocomplete_snippet_intersect() { let intersect_fn: Box = Box::new(crate::std::csg::Intersect); let snippet = intersect_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"intersect([${0:extrude001}, ${1:extrude002}])"#); } #[test] fn get_autocomplete_snippet_get_common_edge() { let get_common_edge_fn: Box = Box::new(crate::std::edge::GetCommonEdge); let snippet = get_common_edge_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"getCommonEdge(faces = [{ value = ${0:"string"}, }])"# ); } #[test] fn get_autocomplete_snippet_scale() { let scale_fn: Box = Box::new(crate::std::transform::Scale); let snippet = scale_fn.to_autocomplete_snippet().unwrap(); assert_eq!(snippet, r#"scale(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})"#); } #[test] fn get_autocomplete_snippet_translate() { let translate_fn: Box = Box::new(crate::std::transform::Translate); let snippet = translate_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"translate(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})"# ); } #[test] fn get_autocomplete_snippet_rotate() { let rotate_fn: Box = Box::new(crate::std::transform::Rotate); let snippet = rotate_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, r#"rotate(${0:%}, roll = ${1:3.14}, pitch = ${2:3.14}, yaw = ${3:3.14})"# ); } #[test] #[allow(clippy::literal_string_with_formatting_args)] fn get_autocomplete_snippet_clone() { let data = kcl_doc::walk_prelude(); let DocData::Fn(clone_fn) = data.find_by_name("clone").unwrap() else { panic!(); }; let snippet = clone_fn.to_autocomplete_snippet(); assert_eq!(snippet, r#"clone(${0:part001})"#); } #[test] fn get_autocomplete_snippet_offset_plane() { let data = kcl_doc::walk_prelude(); let DocData::Fn(offset_plane_fn) = data.find_by_name("offsetPlane").unwrap() else { panic!(); }; let snippet = offset_plane_fn.to_autocomplete_snippet(); assert_eq!(snippet, r#"offsetPlane(${0:XY}, offset = ${1:3.14})"#); } // We want to test the snippets we compile at lsp start. #[test] fn get_all_stdlib_autocomplete_snippets() { let stdlib = crate::std::StdLib::new(); let kcl_std = crate::docs::kcl_doc::walk_prelude(); crate::lsp::kcl::get_completions_from_stdlib(&stdlib, &kcl_std).unwrap(); } // We want to test the signatures we compile at lsp start. #[test] fn get_all_stdlib_signatures() { let stdlib = crate::std::StdLib::new(); let kcl_std = crate::docs::kcl_doc::walk_prelude(); crate::lsp::kcl::get_signatures_from_stdlib(&stdlib, &kcl_std); } #[test] fn get_extrude_signature_help() { let extrude_fn: Box = Box::new(crate::std::extrude::Extrude); let sh = extrude_fn.to_signature_help(); assert_eq!( sh.signatures[0].label, r#"extrude( @sketches: [Sketch], length: number, symmetric?: bool, bidirectionalLength?: number, tagStart?: TagNode, tagEnd?: TagNode, ): [Solid]"# ); } }