Rust side snippet completions (#2096)
* updates; Signed-off-by: Jess Frazelle <github@jessfraz.com> docs Signed-off-by: Jess Frazelle <github@jessfraz.com> remove descriptions Signed-off-by: Jess Frazelle <github@jessfraz.com> get snippet tests Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> more autocomplete tests Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> tab to end of snippets Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * emptu * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * empty * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * empty * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * empty --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -121,9 +121,9 @@ const sketch001 = startSketchOn(box, "END")
|
|||||||
// Angle to revolve (in degrees). Default is 360.
|
// Angle to revolve (in degrees). Default is 360.
|
||||||
angle: number,
|
angle: number,
|
||||||
// Axis of revolution.
|
// Axis of revolution.
|
||||||
axis: "x" |
|
axis: "X" |
|
||||||
"y" |
|
"Y" |
|
||||||
"z" |
|
"Z" |
|
||||||
"-X" |
|
"-X" |
|
||||||
"-Y" |
|
"-Y" |
|
||||||
"-Z" |
|
"-Z" |
|
||||||
|
@ -49363,21 +49363,21 @@
|
|||||||
"description": "X-axis.",
|
"description": "X-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"x"
|
"X"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Y-axis.",
|
"description": "Y-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"y"
|
"Y"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Z-axis.",
|
"description": "Z-axis.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"z"
|
"Z"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -499,7 +499,8 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
// expect there to be three auto complete options
|
// expect there to be three auto complete options
|
||||||
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
|
||||||
await page.getByText('startSketchOn').click()
|
await page.getByText('startSketchOn').click()
|
||||||
await page.keyboard.type("('XY')")
|
await page.keyboard.type("'XY'")
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await page.keyboard.type(' |> startProfi')
|
await page.keyboard.type(' |> startProfi')
|
||||||
// expect there be a single auto complete option that we can just hit enter on
|
// expect there be a single auto complete option that we can just hit enter on
|
||||||
@ -507,7 +508,18 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
|
||||||
|
|
||||||
await page.keyboard.type('([0,0], %)')
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
await page.keyboard.type(' |> lin')
|
await page.keyboard.type(' |> lin')
|
||||||
|
|
||||||
@ -518,14 +530,20 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('Enter')
|
await page.keyboard.press('Enter')
|
||||||
// finish line with comment
|
// finish line with comment
|
||||||
await page.keyboard.type('(5, %) // lin')
|
await page.keyboard.type('5')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.press('ArrowRight')
|
||||||
|
await page.keyboard.type(' // lin')
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
// there shouldn't be any auto complete options for 'lin' in the comment
|
// there shouldn't be any auto complete options for 'lin' in the comment
|
||||||
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('XY')
|
.toHaveText(`const part001 = startSketchOn('XY')
|
||||||
|> startProfileAt([0,0], %)
|
|> startProfileAt([3.14, 3.14], %)
|
||||||
|> xLine(5, %) // lin`)
|
|> xLine(5, %) // lin`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@ -11,7 +11,9 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { indentWithTab } from '@codemirror/commands'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
|
import { foldGutter } from '@codemirror/language'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { processCodeMirrorRanges } from 'lib/selections'
|
import { processCodeMirrorRanges } from 'lib/selections'
|
||||||
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||||
@ -26,6 +28,7 @@ import { ModelingMachineEvent } from 'machines/modelingMachine'
|
|||||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
|
import { Prec } from '@codemirror/state'
|
||||||
|
|
||||||
export const editorShortcutMeta = {
|
export const editorShortcutMeta = {
|
||||||
formatCode: {
|
formatCode: {
|
||||||
@ -175,13 +178,18 @@ export const TextEditor = ({
|
|||||||
]),
|
]),
|
||||||
] as Extension[]
|
] as Extension[]
|
||||||
|
|
||||||
if (kclLSP) extensions.push(kclLSP)
|
if (kclLSP) extensions.push(Prec.highest(kclLSP))
|
||||||
if (copilotLSP) extensions.push(copilotLSP)
|
if (copilotLSP) extensions.push(copilotLSP)
|
||||||
|
|
||||||
|
// We do the tab keymap after the LSPs with the lowest precedence.
|
||||||
|
// Specifically, tab for in a completion.
|
||||||
|
extensions.push(Prec.lowest(keymap.of([indentWithTab])))
|
||||||
|
|
||||||
// These extensions have proven to mess with vitest
|
// These extensions have proven to mess with vitest
|
||||||
if (!TEST) {
|
if (!TEST) {
|
||||||
extensions.push(
|
extensions.push(
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
|
foldGutter(),
|
||||||
linter((_view) => {
|
linter((_view) => {
|
||||||
return kclErrToDiagnostic(errors)
|
return kclErrToDiagnostic(errors)
|
||||||
}),
|
}),
|
||||||
@ -238,6 +246,7 @@ export const TextEditor = ({
|
|||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
onCreateEditor={(_editorView) => setEditorView(_editorView)}
|
||||||
|
indentWithTab={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -36,6 +36,7 @@ export function kclPlugin(options: LanguageServerOptions): Extension {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
}),
|
}),
|
||||||
autocompletion({
|
autocompletion({
|
||||||
|
defaultKeymap: true,
|
||||||
override: [
|
override: [
|
||||||
async (context) => {
|
async (context) => {
|
||||||
if (plugin == null) return null
|
if (plugin == null) return null
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { completeFromList } from '@codemirror/autocomplete'
|
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
|
||||||
import { setDiagnostics } from '@codemirror/lint'
|
import { setDiagnostics } from '@codemirror/lint'
|
||||||
import { Facet } from '@codemirror/state'
|
import { Facet } from '@codemirror/state'
|
||||||
import { EditorView, Tooltip } from '@codemirror/view'
|
import { EditorView, Tooltip } from '@codemirror/view'
|
||||||
@ -224,6 +224,10 @@ export class LanguageServerPlugin implements PluginValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (insertText && insertTextFormat === 2) {
|
||||||
|
return snippetCompletion(insertText, completion)
|
||||||
|
}
|
||||||
|
|
||||||
return completion
|
return completion
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -57,11 +57,17 @@ impl StdLibFnArg {
|
|||||||
get_type_string_from_schema(&self.schema.clone())
|
get_type_string_from_schema(&self.schema.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn get_autocomplete_string(&self) -> Result<String> {
|
pub fn get_autocomplete_string(&self) -> Result<String> {
|
||||||
get_autocomplete_string_from_schema(&self.schema.clone())
|
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_ == "ExtrudeGroup" || self.type_ == "SketchSurface" {
|
||||||
|
return Ok(Some((index, format!("${{{}:{}}}", index, "%"))));
|
||||||
|
}
|
||||||
|
get_autocomplete_snippet_from_schema(&self.schema.clone(), index)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn description(&self) -> Option<String> {
|
pub fn description(&self) -> Option<String> {
|
||||||
get_description_string_from_schema(&self.schema.clone())
|
get_description_string_from_schema(&self.schema.clone())
|
||||||
}
|
}
|
||||||
@ -153,8 +159,8 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
signature
|
signature
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_completion_item(&self) -> CompletionItem {
|
fn to_completion_item(&self) -> Result<CompletionItem> {
|
||||||
CompletionItem {
|
Ok(CompletionItem {
|
||||||
label: self.name(),
|
label: self.name(),
|
||||||
label_details: Some(CompletionItemLabelDetails {
|
label_details: Some(CompletionItemLabelDetails {
|
||||||
detail: Some(self.fn_signature().replace(&self.name(), "")),
|
detail: Some(self.fn_signature().replace(&self.name(), "")),
|
||||||
@ -174,25 +180,7 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
preselect: None,
|
preselect: None,
|
||||||
sort_text: None,
|
sort_text: None,
|
||||||
filter_text: None,
|
filter_text: None,
|
||||||
insert_text: Some(format!(
|
insert_text: Some(self.to_autocomplete_snippet()?),
|
||||||
"{}({})",
|
|
||||||
self.name(),
|
|
||||||
self.args()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
// It is okay to unwrap here since in the `kcl-lib` tests, we would have caught
|
|
||||||
// any errors in the `self`'s signature.
|
|
||||||
.map(|(index, item)| {
|
|
||||||
let format = item.get_autocomplete_string().unwrap();
|
|
||||||
if item.type_ == "SketchGroup" || item.type_ == "ExtrudeGroup" {
|
|
||||||
format!("${{{}:{}}}", index + 1, "%")
|
|
||||||
} else {
|
|
||||||
format!("${{{}:{}}}", index + 1, format)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",")
|
|
||||||
)),
|
|
||||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||||
insert_text_mode: None,
|
insert_text_mode: None,
|
||||||
text_edit: None,
|
text_edit: None,
|
||||||
@ -201,7 +189,21 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
commit_characters: None,
|
commit_characters: None,
|
||||||
data: None,
|
data: None,
|
||||||
tags: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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 {
|
fn to_signature_help(&self) -> SignatureHelp {
|
||||||
@ -350,7 +352,13 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
|
|||||||
match array_val.max_items {
|
match array_val.max_items {
|
||||||
Some(val) => {
|
Some(val) => {
|
||||||
return Ok((
|
return Ok((
|
||||||
format!("[{}]", (0..val).map(|_| "number").collect::<Vec<_>>().join(", ")),
|
format!(
|
||||||
|
"[{}]",
|
||||||
|
(0..val)
|
||||||
|
.map(|_| get_type_string_from_schema(items).unwrap().0)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
),
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -429,9 +437,176 @@ pub fn get_type_string_from_schema(schema: &schemars::schema::Schema) -> Result<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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" || format == "uint" || format == "int64" || format == "uint32" {
|
||||||
|
return Ok(Some((index, format!(r#"${{{}:3.14}}"#, 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.
|
||||||
|
for (i, (prop_name, prop)) in obj_val.properties.iter().enumerate() {
|
||||||
|
if prop_name.starts_with('_') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((_, snippet)) = get_autocomplete_snippet_from_schema(prop, index + i)? {
|
||||||
|
fn_docs.push_str(&format!("\t{}: {},\n", prop_name, snippet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn_docs.push('}');
|
||||||
|
|
||||||
|
return Ok(Some((index + obj_val.properties.len() - 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::<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(_string)) = &o.instance_type {
|
||||||
|
return Ok(Some((index, format!(r#"${{{}:"string"}}"#, index))));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) -> Result<String> {
|
||||||
match schema {
|
match schema {
|
||||||
schemars::schema::Schema::Object(o) => {
|
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 let Some(format) = &o.format {
|
||||||
if format == "uuid" {
|
if format == "uuid" {
|
||||||
return Ok(Primitive::Uuid.to_string());
|
return Ok(Primitive::Uuid.to_string());
|
||||||
@ -451,9 +626,6 @@ pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) ->
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(description) = get_description_string_from_schema(prop) {
|
|
||||||
fn_docs.push_str(&format!("\t// {}\n", description));
|
|
||||||
}
|
|
||||||
fn_docs.push_str(&format!(
|
fn_docs.push_str(&format!(
|
||||||
"\t{}: {},\n",
|
"\t{}: {},\n",
|
||||||
prop_name,
|
prop_name,
|
||||||
@ -469,7 +641,17 @@ pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) ->
|
|||||||
if let Some(array_val) = &o.array {
|
if let Some(array_val) = &o.array {
|
||||||
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
|
if let Some(schemars::schema::SingleOrVec::Single(items)) = &array_val.items {
|
||||||
// Let's print out the object's properties.
|
// 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)?));
|
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
|
||||||
|
}
|
||||||
|
};
|
||||||
} else if let Some(items) = &array_val.contains {
|
} else if let Some(items) = &array_val.contains {
|
||||||
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
|
return Ok(format!("[{}]", get_autocomplete_string_from_schema(items)?));
|
||||||
}
|
}
|
||||||
@ -478,7 +660,36 @@ pub fn get_autocomplete_string_from_schema(schema: &schemars::schema::Schema) ->
|
|||||||
if let Some(subschemas) = &o.subschemas {
|
if let Some(subschemas) = &o.subschemas {
|
||||||
let mut fn_docs = String::new();
|
let mut fn_docs = String::new();
|
||||||
if let Some(items) = &subschemas.one_of {
|
if let Some(items) = &subschemas.one_of {
|
||||||
if let Some(item) = items.iter().next() {
|
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.
|
// Let's print out the object's properties.
|
||||||
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
|
fn_docs.push_str(&get_autocomplete_string_from_schema(item)?);
|
||||||
}
|
}
|
||||||
@ -556,6 +767,8 @@ pub fn completion_item_from_enum_schema(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::StdLibFn;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_function() {
|
fn test_serialize_function() {
|
||||||
let some_function = crate::ast::types::Function::StdLib {
|
let some_function = crate::ast::types::Function::StdLib {
|
||||||
@ -577,4 +790,66 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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:3.14},
|
||||||
|
rotateDuplicates: ${4:"string"},
|
||||||
|
}, ${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: ${1:"X"},
|
||||||
|
}, ${2:%})${}"#
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -793,7 +793,7 @@ pub fn get_completions_from_stdlib(stdlib: &crate::std::StdLib) -> Result<HashMa
|
|||||||
let combined = stdlib.combined();
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
for internal_fn in combined.values() {
|
for internal_fn in combined.values() {
|
||||||
completions.insert(internal_fn.name(), internal_fn.to_completion_item());
|
completions.insert(internal_fn.name(), internal_fn.to_completion_item()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let variable_kinds = VariableKind::to_completion_items()?;
|
let variable_kinds = VariableKind::to_completion_items()?;
|
||||||
|
@ -45,13 +45,13 @@ pub enum RevolveAxis {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum RevolveAxisAndOrigin {
|
pub enum RevolveAxisAndOrigin {
|
||||||
/// X-axis.
|
/// X-axis.
|
||||||
#[serde(alias = "X")]
|
#[serde(rename = "X", alias = "x")]
|
||||||
X,
|
X,
|
||||||
/// Y-axis.
|
/// Y-axis.
|
||||||
#[serde(alias = "Y")]
|
#[serde(rename = "Y", alias = "y")]
|
||||||
Y,
|
Y,
|
||||||
/// Z-axis.
|
/// Z-axis.
|
||||||
#[serde(alias = "Z")]
|
#[serde(rename = "Z", alias = "z")]
|
||||||
Z,
|
Z,
|
||||||
/// Flip the X-axis.
|
/// Flip the X-axis.
|
||||||
#[serde(rename = "-X", alias = "-x")]
|
#[serde(rename = "-X", alias = "-x")]
|
||||||
@ -337,7 +337,7 @@ mod tests {
|
|||||||
fn test_deserialize_revolve_axis() {
|
fn test_deserialize_revolve_axis() {
|
||||||
let data = RevolveAxis::Axis(RevolveAxisAndOrigin::X);
|
let data = RevolveAxis::Axis(RevolveAxisAndOrigin::X);
|
||||||
let mut str_json = serde_json::to_string(&data).unwrap();
|
let mut str_json = serde_json::to_string(&data).unwrap();
|
||||||
assert_eq!(str_json, "\"x\"");
|
assert_eq!(str_json, "\"X\"");
|
||||||
|
|
||||||
str_json = "\"Y\"".to_string();
|
str_json = "\"Y\"".to_string();
|
||||||
let data: RevolveAxis = serde_json::from_str(&str_json).unwrap();
|
let data: RevolveAxis = serde_json::from_str(&str_json).unwrap();
|
||||||
|