Files
modeling-app/rust/kcl-lib/src/docs/mod.rs
Andrew Varga d34aea345b #6548 Fix autocomplete for offsetPlane (#6940)
* handle Plane in get_autocomplete_snippet to fix missing default plane value for offsetPlane

* adds test for offsetPlane default plane value

* cleanup test

* use XY instead of XZ for default plane

* add rust test for offsetplane autocomplete

* remove playwright test for offestPlane, as it has rust test now and there are autocomplete tests already
2025-05-15 19:53:35 +02:00

1213 lines
45 KiB
Rust

//! 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::<NumericType>().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<String>,
/// If this function uses keyword arguments, or positional arguments.
pub keyword_arguments: bool,
/// The args of the function.
pub args: Vec<StdLibFnArg>,
/// The return value of the function.
pub return_value: Option<StdLibFnArg>,
/// 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<bool> {
is_primitive(&self.schema.schema.clone().into()).map(|r| r.is_some())
}
pub fn get_autocomplete_string(&self) -> Result<String> {
get_autocomplete_string_from_schema(&self.schema.schema.clone().into())
}
pub fn get_autocomplete_snippet(&self, index: usize, in_keyword_fn: bool) -> Result<Option<(usize, String)>> {
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<String> {
// 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<Option<(usize, String)>> {
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::<Sketch>().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::<Vec<_>>()
.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<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((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<StdLibFnArg> 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<String> {
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<String>;
/// The args of the function.
fn args(&self, inline_subschemas: bool) -> Vec<StdLibFnArg>;
/// The return value of the function.
fn return_value(&self, inline_subschemas: bool) -> Option<StdLibFnArg>;
/// 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<dyn StdLibFn>;
/// Return a JSON struct representing the function.
fn to_json(&self) -> Result<StdLibFnData> {
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<CompletionItem> {
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<String> {
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::<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::RootSchema) -> Option<String> {
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<Option<Primitive>> {
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<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(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::<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() {
// 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<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() {
// We test this one specifically because it has ints and floats and strings.
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(${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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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<dyn StdLibFn> = 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]"#
);
}
}