Fix vscode lsp bugs (#6271)

* start of shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* untitled test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-04-10 19:53:05 -07:00
committed by GitHub
parent 6d50278d34
commit 1b8eee86a1
5 changed files with 184 additions and 36 deletions

View File

@ -389,6 +389,13 @@ export class LanguageServerPlugin implements PluginValue {
} }
if (insertText && insertTextFormat === 2) { 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) return snippetCompletion(insertText, completion)
} }

View File

@ -9,7 +9,10 @@ export async function createClient(
serverOptions: lc.ServerOptions serverOptions: lc.ServerOptions
): Promise<lc.LanguageClient> { ): Promise<lc.LanguageClient> {
const clientOptions: lc.LanguageClientOptions = { const clientOptions: lc.LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'kcl' }], documentSelector: [
{ scheme: 'file', language: 'kcl' },
{ scheme: 'untitled', language: 'kcl' },
],
initializationOptions, initializationOptions,
traceOutputChannel, traceOutputChannel,
outputChannel, outputChannel,

View File

@ -447,9 +447,9 @@ impl FnData {
#[allow(clippy::literal_string_with_formatting_args)] #[allow(clippy::literal_string_with_formatting_args)]
pub(super) fn to_autocomplete_snippet(&self) -> String { pub(super) fn to_autocomplete_snippet(&self) -> String {
if self.name == "loft" { 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" { } 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 args = Vec::new();
let mut index = 0; let mut index = 0;
@ -459,9 +459,7 @@ impl FnData {
args.push(arg_str); args.push(arg_str);
} }
} }
// We end with ${} so you can jump to the end of the snippet. format!("{}({})", self.preferred_name, args.join(", "))
// After the last argument.
format!("{}({})${{}}", self.preferred_name, args.join(", "))
} }
fn to_signature_help(&self) -> SignatureHelp { fn to_signature_help(&self) -> SignatureHelp {

View File

@ -501,9 +501,9 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
#[allow(clippy::literal_string_with_formatting_args)] #[allow(clippy::literal_string_with_formatting_args)]
fn to_autocomplete_snippet(&self) -> Result<String> { fn to_autocomplete_snippet(&self) -> Result<String> {
if self.name() == "loft" { 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" { } 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 in_keyword_fn = self.keyword_arguments();
let mut args = Vec::new(); let mut args = Vec::new();
@ -514,9 +514,7 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
args.push(arg_str); args.push(arg_str);
} }
} }
// We end with ${} so you can jump to the end of the snippet. Ok(format!("{}({})", self.name(), args.join(", ")))
// After the last argument.
Ok(format!("{}({})${{}}", self.name(), args.join(", ")))
} }
fn to_signature_help(&self) -> SignatureHelp { fn to_signature_help(&self) -> SignatureHelp {
@ -894,7 +892,7 @@ mod tests {
fn get_autocomplete_snippet_line() { fn get_autocomplete_snippet_line() {
let line_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Line); let line_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Line);
let snippet = line_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -902,7 +900,7 @@ mod tests {
fn get_autocomplete_snippet_extrude() { fn get_autocomplete_snippet_extrude() {
let extrude_fn: Box<dyn StdLibFn> = Box::new(crate::std::extrude::Extrude); let extrude_fn: Box<dyn StdLibFn> = Box::new(crate::std::extrude::Extrude);
let snippet = extrude_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -912,7 +910,7 @@ mod tests {
let snippet = fillet_fn.to_autocomplete_snippet().unwrap(); let snippet = fillet_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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() { fn get_autocomplete_snippet_start_sketch_on() {
let start_sketch_on_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::StartSketchOn); let start_sketch_on_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::StartSketchOn);
let snippet = start_sketch_on_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -931,7 +929,7 @@ mod tests {
let snippet = pattern_fn.to_autocomplete_snippet().unwrap(); let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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!(); panic!();
}; };
let snippet = revolve_fn.to_autocomplete_snippet(); let snippet = revolve_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"revolve(axis = ${0:X})${}"#); assert_eq!(snippet, r#"revolve(axis = ${0:X})"#);
} }
#[test] #[test]
@ -955,7 +953,7 @@ mod tests {
let snippet = circle_fn.to_autocomplete_snippet(); let snippet = circle_fn.to_autocomplete_snippet();
assert_eq!( assert_eq!(
snippet, 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}, angleStart = ${0:3.14},
angleEnd = ${1:3.14}, angleEnd = ${1:3.14},
radius = ${2:3.14}, radius = ${2:3.14},
}, ${3:%})${}"# }, ${3:%})"#
); );
} }
@ -978,7 +976,7 @@ mod tests {
fn get_autocomplete_snippet_map() { fn get_autocomplete_snippet_map() {
let map_fn: Box<dyn StdLibFn> = Box::new(crate::std::array::Map); let map_fn: Box<dyn StdLibFn> = Box::new(crate::std::array::Map);
let snippet = map_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -988,7 +986,7 @@ mod tests {
let snippet = pattern_fn.to_autocomplete_snippet().unwrap(); let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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(); let snippet = appearance_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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() { fn get_autocomplete_snippet_loft() {
let loft_fn: Box<dyn StdLibFn> = Box::new(crate::std::loft::Loft); let loft_fn: Box<dyn StdLibFn> = Box::new(crate::std::loft::Loft);
let snippet = loft_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -1015,7 +1013,7 @@ mod tests {
fn get_autocomplete_snippet_sweep() { fn get_autocomplete_snippet_sweep() {
let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep); let sweep_fn: Box<dyn StdLibFn> = Box::new(crate::std::sweep::Sweep);
let snippet = sweep_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -1023,7 +1021,7 @@ mod tests {
fn get_autocomplete_snippet_hole() { fn get_autocomplete_snippet_hole() {
let hole_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Hole); let hole_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::Hole);
let snippet = hole_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -1036,7 +1034,7 @@ mod tests {
let snippet = helix_fn.to_autocomplete_snippet(); let snippet = helix_fn.to_autocomplete_snippet();
assert_eq!( assert_eq!(
snippet, 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() { fn get_autocomplete_snippet_union() {
let union_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Union); let union_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Union);
let snippet = union_fn.to_autocomplete_snippet().unwrap(); let snippet = union_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"union(${0:%})${}"#); assert_eq!(snippet, r#"union(${0:%})"#);
} }
#[test] #[test]
@ -1053,7 +1051,7 @@ mod tests {
fn get_autocomplete_snippet_subtract() { fn get_autocomplete_snippet_subtract() {
let subtract_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Subtract); let subtract_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Subtract);
let snippet = subtract_fn.to_autocomplete_snippet().unwrap(); 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] #[test]
@ -1061,7 +1059,7 @@ mod tests {
fn get_autocomplete_snippet_intersect() { fn get_autocomplete_snippet_intersect() {
let intersect_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Intersect); let intersect_fn: Box<dyn StdLibFn> = Box::new(crate::std::csg::Intersect);
let snippet = intersect_fn.to_autocomplete_snippet().unwrap(); let snippet = intersect_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"intersect(${0:%})${}"#); assert_eq!(snippet, r#"intersect(${0:%})"#);
} }
#[test] #[test]
@ -1073,7 +1071,7 @@ mod tests {
snippet, snippet,
r#"getCommonEdge(faces = [{ r#"getCommonEdge(faces = [{
value = ${0:"string"}, value = ${0:"string"},
}])${}"# }])"#
); );
} }
@ -1082,10 +1080,7 @@ mod tests {
fn get_autocomplete_snippet_scale() { fn get_autocomplete_snippet_scale() {
let scale_fn: Box<dyn StdLibFn> = Box::new(crate::std::transform::Scale); let scale_fn: Box<dyn StdLibFn> = Box::new(crate::std::transform::Scale);
let snippet = scale_fn.to_autocomplete_snippet().unwrap(); let snippet = scale_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(snippet, r#"scale(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})"#);
snippet,
r#"scale(${0:%}, x = ${1:3.14}, y = ${2:3.14}, z = ${3:3.14})${}"#
);
} }
#[test] #[test]
@ -1095,7 +1090,7 @@ mod tests {
let snippet = translate_fn.to_autocomplete_snippet().unwrap(); let snippet = translate_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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(); let snippet = rotate_fn.to_autocomplete_snippet().unwrap();
assert_eq!( assert_eq!(
snippet, 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})"#
); );
} }

View File

@ -3418,3 +3418,148 @@ async fn kcl_test_kcl_lsp_multi_file_error() {
server.executor_ctx().await.clone().unwrap().close().await; 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;
}