diff --git a/packages/codemirror-lsp-client/src/plugin/lsp.ts b/packages/codemirror-lsp-client/src/plugin/lsp.ts index b68c8a783..12079d9e5 100644 --- a/packages/codemirror-lsp-client/src/plugin/lsp.ts +++ b/packages/codemirror-lsp-client/src/plugin/lsp.ts @@ -389,6 +389,13 @@ export class LanguageServerPlugin implements PluginValue { } if (insertText && insertTextFormat === 2) { + // We end with ${} so you can jump to the end of the snippet. + // After the last argument. + // This is not standard from the lsp so we add it here. + if (insertText.endsWith(')')) { + // We have a function its safe to insert the ${} at the end. + insertText = insertText + '${}' + } return snippetCompletion(insertText, completion) } diff --git a/rust/kcl-language-server/client/src/client.ts b/rust/kcl-language-server/client/src/client.ts index 772103c34..65c3b2dfd 100644 --- a/rust/kcl-language-server/client/src/client.ts +++ b/rust/kcl-language-server/client/src/client.ts @@ -9,7 +9,10 @@ export async function createClient( serverOptions: lc.ServerOptions ): Promise { const clientOptions: lc.LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'kcl' }], + documentSelector: [ + { scheme: 'file', language: 'kcl' }, + { scheme: 'untitled', language: 'kcl' }, + ], initializationOptions, traceOutputChannel, outputChannel, diff --git a/rust/kcl-lib/src/docs/kcl_doc.rs b/rust/kcl-lib/src/docs/kcl_doc.rs index 419727f41..0d26fded4 100644 --- a/rust/kcl-lib/src/docs/kcl_doc.rs +++ b/rust/kcl-lib/src/docs/kcl_doc.rs @@ -447,9 +447,9 @@ impl FnData { #[allow(clippy::literal_string_with_formatting_args)] pub(super) fn to_autocomplete_snippet(&self) -> String { if self.name == "loft" { - return "loft([${0:sketch000}, ${1:sketch001}])${}".to_owned(); + return "loft([${0:sketch000}, ${1:sketch001}])".to_owned(); } else if self.name == "hole" { - return "hole(${0:holeSketch}, ${1:%})${}".to_owned(); + return "hole(${0:holeSketch}, ${1:%})".to_owned(); } let mut args = Vec::new(); let mut index = 0; @@ -459,9 +459,7 @@ impl FnData { args.push(arg_str); } } - // We end with ${} so you can jump to the end of the snippet. - // After the last argument. - format!("{}({})${{}}", self.preferred_name, args.join(", ")) + format!("{}({})", self.preferred_name, args.join(", ")) } fn to_signature_help(&self) -> SignatureHelp { diff --git a/rust/kcl-lib/src/docs/mod.rs b/rust/kcl-lib/src/docs/mod.rs index efd500f39..bfdb33afe 100644 --- a/rust/kcl-lib/src/docs/mod.rs +++ b/rust/kcl-lib/src/docs/mod.rs @@ -501,9 +501,9 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync { #[allow(clippy::literal_string_with_formatting_args)] fn to_autocomplete_snippet(&self) -> Result { if self.name() == "loft" { - return Ok("loft([${0:sketch000}, ${1:sketch001}])${}".to_string()); + return Ok("loft([${0:sketch000}, ${1:sketch001}])".to_string()); } else if self.name() == "hole" { - return Ok("hole(${0:holeSketch}, ${1:%})${}".to_string()); + return Ok("hole(${0:holeSketch}, ${1:%})".to_string()); } let in_keyword_fn = self.keyword_arguments(); let mut args = Vec::new(); @@ -514,9 +514,7 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync { 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(", "))) + Ok(format!("{}({})", self.name(), args.join(", "))) } fn to_signature_help(&self) -> SignatureHelp { @@ -894,7 +892,7 @@ mod tests { 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}])${}"#); + assert_eq!(snippet, r#"line(${0:%}, end = [${1:3.14}, ${2:3.14}])"#); } #[test] @@ -902,7 +900,7 @@ mod tests { 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})${}"#); + assert_eq!(snippet, r#"extrude(${0:%}, length = ${1:3.14})"#); } #[test] @@ -912,7 +910,7 @@ mod tests { let snippet = fillet_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, - r#"fillet(${0:%}, radius = ${1:3.14}, tags = [${2:"tag_or_edge_fn"}])${}"# + r#"fillet(${0:%}, radius = ${1:3.14}, tags = [${2:"tag_or_edge_fn"}])"# ); } @@ -920,7 +918,7 @@ mod tests { 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"})${}"#); + assert_eq!(snippet, r#"startSketchOn(${0:"XY"})"#); } #[test] @@ -931,7 +929,7 @@ mod tests { 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})${}"# + 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})"# ); } @@ -942,7 +940,7 @@ mod tests { panic!(); }; let snippet = revolve_fn.to_autocomplete_snippet(); - assert_eq!(snippet, r#"revolve(axis = ${0:X})${}"#); + assert_eq!(snippet, r#"revolve(axis = ${0:X})"#); } #[test] @@ -955,7 +953,7 @@ mod tests { let snippet = circle_fn.to_autocomplete_snippet(); assert_eq!( snippet, - r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})${}"# + r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})"# ); } @@ -970,7 +968,7 @@ mod tests { angleStart = ${0:3.14}, angleEnd = ${1:3.14}, radius = ${2:3.14}, -}, ${3:%})${}"# +}, ${3:%})"# ); } @@ -978,7 +976,7 @@ mod tests { fn get_autocomplete_snippet_map() { let map_fn: Box = Box::new(crate::std::array::Map); let snippet = map_fn.to_autocomplete_snippet().unwrap(); - assert_eq!(snippet, r#"map(${0:[0..9]})${}"#); + assert_eq!(snippet, r#"map(${0:[0..9]})"#); } #[test] @@ -988,7 +986,7 @@ mod tests { 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}])${}"# + r#"patternLinear2d(${0:%}, instances = ${1:10}, distance = ${2:3.14}, axis = [${3:3.14}, ${4:3.14}])"# ); } @@ -998,7 +996,7 @@ mod tests { let snippet = appearance_fn.to_autocomplete_snippet().unwrap(); assert_eq!( snippet, - r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})${}"# + r#"appearance(${0:%}, color = ${1:"#.to_owned() + "\"#" + r#"ff0000"})"# ); } @@ -1007,7 +1005,7 @@ mod tests { 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}])${}"#); + assert_eq!(snippet, r#"loft([${0:sketch000}, ${1:sketch001}])"#); } #[test] @@ -1015,7 +1013,7 @@ mod tests { 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})${}"#); + assert_eq!(snippet, r#"sweep(${0:%}, path = ${1:sketch000})"#); } #[test] @@ -1023,7 +1021,7 @@ mod tests { fn get_autocomplete_snippet_hole() { let hole_fn: Box = Box::new(crate::std::sketch::Hole); let snippet = hole_fn.to_autocomplete_snippet().unwrap(); - assert_eq!(snippet, r#"hole(${0:holeSketch}, ${1:%})${}"#); + assert_eq!(snippet, r#"hole(${0:holeSketch}, ${1:%})"#); } #[test] @@ -1036,7 +1034,7 @@ mod tests { 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})${}"# + r#"helix(revolutions = ${0:3.14}, angleStart = ${1:3.14}, radius = ${2:3.14}, axis = ${3:X}, length = ${4:3.14})"# ); } @@ -1045,7 +1043,7 @@ mod tests { 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:%})${}"#); + assert_eq!(snippet, r#"union(${0:%})"#); } #[test] @@ -1053,7 +1051,7 @@ mod tests { 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:%}, tools = ${1:%})${}"#); + assert_eq!(snippet, r#"subtract(${0:%}, tools = ${1:%})"#); } #[test] @@ -1061,7 +1059,7 @@ mod tests { 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:%})${}"#); + assert_eq!(snippet, r#"intersect(${0:%})"#); } #[test] @@ -1073,7 +1071,7 @@ mod tests { snippet, r#"getCommonEdge(faces = [{ value = ${0:"string"}, -}])${}"# +}])"# ); } @@ -1082,10 +1080,7 @@ mod tests { 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})${}"# - ); + assert_eq!(snippet, r#"scale(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})"#); } #[test] @@ -1095,7 +1090,7 @@ mod tests { 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})${}"# + r#"translate(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})"# ); } @@ -1106,7 +1101,7 @@ mod tests { 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})${}"# + r#"rotate(${0:%}, roll = ${1:3.14}, pitch = ${2:3.14}, yaw = ${3:3.14})"# ); } diff --git a/rust/kcl-lib/src/lsp/tests.rs b/rust/kcl-lib/src/lsp/tests.rs index 48cd58cb3..6554f6a76 100644 --- a/rust/kcl-lib/src/lsp/tests.rs +++ b/rust/kcl-lib/src/lsp/tests.rs @@ -3418,3 +3418,148 @@ async fn kcl_test_kcl_lsp_multi_file_error() { server.executor_ctx().await.clone().unwrap().close().await; } + +#[tokio::test(flavor = "multi_thread")] +async fn test_kcl_lsp_on_hover_untitled_file_scheme() { + let server = kcl_lsp_server(true).await.unwrap(); + + // Send open file. + server + .did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams { + text_document: tower_lsp::lsp_types::TextDocumentItem { + uri: "untitled:Untitled-1".try_into().unwrap(), + language_id: "kcl".to_string(), + version: 1, + text: r#"startSketchOn(XY) +foo = 42 +foo + +fn bar(x: string): string { + return x +} + +bar("an arg") + +startSketchOn(XY) + |> startProfileAt([0, 0], %) + |> line(end = [10, 0]) + |> line(end = [0, 10]) +"# + .to_string(), + }, + }) + .await; + + // Std lib call + let hover = server + .hover(tower_lsp::lsp_types::HoverParams { + text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "untitled:Untitled-1".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { line: 0, character: 2 }, + }, + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + match hover.unwrap().contents { + tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { + assert!(value.contains("startSketchOn")); + assert!(value.contains(": SketchSurface")); + assert!(value.contains("Start a new 2-dimensional sketch on a specific")); + } + _ => unreachable!(), + } + + // Variable use + let hover = server + .hover(tower_lsp::lsp_types::HoverParams { + text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "untitled:Untitled-1".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { line: 2, character: 1 }, + }, + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + match hover.unwrap().contents { + tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { + assert!(value.contains("foo: number = 42")); + } + _ => unreachable!(), + } + + // User-defined function call. + let hover = server + .hover(tower_lsp::lsp_types::HoverParams { + text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "untitled:Untitled-1".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { line: 8, character: 1 }, + }, + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + match hover.unwrap().contents { + tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { + assert!(value.contains("bar(x: string): string")); + } + _ => unreachable!(), + } + + // Variable inside a function + let hover = server + .hover(tower_lsp::lsp_types::HoverParams { + text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "untitled:Untitled-1".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { line: 5, character: 9 }, + }, + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + match hover.unwrap().contents { + tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { + assert!(value.contains("x: string")); + } + _ => unreachable!(), + } + + // std function KwArg + let hover = server + .hover(tower_lsp::lsp_types::HoverParams { + text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams { + text_document: tower_lsp::lsp_types::TextDocumentIdentifier { + uri: "untitled:Untitled-1".try_into().unwrap(), + }, + position: tower_lsp::lsp_types::Position { + line: 12, + character: 11, + }, + }, + work_done_progress_params: Default::default(), + }) + .await + .unwrap(); + + match hover.unwrap().contents { + tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { + assert!(value.contains("end?: [number]")); + assert!(value.contains("How far away (along the X and Y axes) should this line go?")); + } + _ => unreachable!(), + } + + server.executor_ctx().await.clone().unwrap().close().await; +}