Code completions for @settings (#5622)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-03-05 16:23:46 +13:00
committed by GitHub
parent de85c31e71
commit e8af61e11f
5 changed files with 69 additions and 37 deletions

View File

@ -24,6 +24,10 @@ pub(super) const IMPORT_COORDS_VALUES: [(&str, &System); 3] =
[("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)]; [("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)];
pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit"; pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit";
pub(crate) fn settings_completion_text() -> String {
format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = mm, {SETTINGS_UNIT_ANGLE} = deg)")
}
pub(super) fn is_significant(attr: &&Node<Annotation>) -> bool { pub(super) fn is_significant(attr: &&Node<Annotation>) -> bool {
match attr.name() { match attr.name() {
Some(name) => SIGNIFICANT_ATTRS.contains(&name), Some(name) => SIGNIFICANT_ATTRS.contains(&name),

View File

@ -1247,13 +1247,13 @@ impl LanguageServer for Backend {
}; };
let position = position_to_char_index(params.text_document_position.position, current_code); let position = position_to_char_index(params.text_document_position.position, current_code);
if ast.ast.get_non_code_meta_for_position(position).is_some() { if ast.ast.in_comment(position) {
// If we are in a code comment we don't want to show completions. // If we are in a code comment we don't want to show completions.
return Ok(None); return Ok(None);
} }
// Get the completion items for the ast. // Get the completion items for the ast.
let Ok(variables) = ast.ast.completion_items() else { let Ok(variables) = ast.ast.completion_items(position) else {
return Ok(Some(CompletionResponse::Array(completions))); return Ok(Some(CompletionResponse::Array(completions)));
}; };

View File

@ -644,7 +644,10 @@ async fn test_kcl_lsp_completions() {
uri: "file:///test.kcl".try_into().unwrap(), uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(), language_id: "kcl".to_string(),
version: 1, version: 1,
text: r#"thing= 1 // Blank lines to check that we get completions even in an AST newline thing.
text: r#"
thing= 1
st"# st"#
.to_string(), .to_string(),
}, },
@ -658,7 +661,7 @@ st"#
text_document: tower_lsp::lsp_types::TextDocumentIdentifier { text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(), uri: "file:///test.kcl".try_into().unwrap(),
}, },
position: tower_lsp::lsp_types::Position { line: 0, character: 16 }, position: tower_lsp::lsp_types::Position { line: 0, character: 0 },
}, },
context: None, context: None,
partial_result_params: Default::default(), partial_result_params: Default::default(),
@ -671,6 +674,7 @@ st"#
// Check the completions. // Check the completions.
if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions { if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions {
assert!(completions.len() > 10); assert!(completions.len() > 10);
assert!(completions.iter().any(|c| c.label == "@settings"));
} else { } else {
panic!("Expected array of completions"); panic!("Expected array of completions");
} }

View File

@ -190,7 +190,7 @@ pub struct Program {
impl Node<Program> { impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items. /// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> { pub fn completion_items<'a>(&'a self, position: usize) -> Result<Vec<CompletionItem>> {
let completions = Rc::new(RefCell::new(vec![])); let completions = Rc::new(RefCell::new(vec![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| { crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = completions.borrow_mut(); let mut findings = completions.borrow_mut();
@ -208,8 +208,20 @@ impl Node<Program> {
} }
Ok::<bool, anyhow::Error>(true) Ok::<bool, anyhow::Error>(true)
})?; })?;
let x = completions.take(); let mut completions = completions.take();
Ok(x.clone())
if self.body.is_empty() || position <= self.body[0].start() {
// The cursor is before any items in the body, we can suggest the settings annotation as a completion.
completions.push(CompletionItem {
label: "@settings".to_owned(),
kind: Some(CompletionItemKind::STRUCT),
detail: Some("Settings attribute".to_owned()),
insert_text: Some(crate::execution::annotations::settings_completion_text()),
insert_text_format: Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET),
..CompletionItem::default()
});
}
Ok(completions)
} }
/// Returns all the lsp symbols in the program. /// Returns all the lsp symbols in the program.
@ -347,32 +359,34 @@ impl Program {
} }
} }
/// Returns a non code meta that includes the given character position. pub fn in_comment(&self, pos: usize) -> bool {
pub fn get_non_code_meta_for_position(&self, pos: usize) -> Option<&NonCodeMeta> {
// Check if its in the body. // Check if its in the body.
if self.non_code_meta.contains(pos) { if self.non_code_meta.in_comment(pos) {
return Some(&self.non_code_meta); return true;
} }
let item = self.get_body_item_for_position(pos)?; let item = self.get_body_item_for_position(pos);
// Recurse over the item. // Recurse over the item.
let expr = match item { let expr = match item {
BodyItem::ImportStatement(_) => None, Some(BodyItem::ImportStatement(_)) => None,
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression), Some(BodyItem::ExpressionStatement(expression_statement)) => Some(&expression_statement.expression),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos), Some(BodyItem::VariableDeclaration(variable_declaration)) => {
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument), variable_declaration.get_expr_for_position(pos)
}
Some(BodyItem::ReturnStatement(return_statement)) => Some(&return_statement.argument),
None => return false,
}; };
// Check if the expr's non code meta contains the position. // Check if the expr's non code meta contains the position.
if let Some(expr) = expr { if let Some(expr) = expr {
if let Some(non_code_meta) = expr.get_non_code_meta() { if let Some(non_code_meta) = expr.get_non_code_meta() {
if non_code_meta.contains(pos) { if non_code_meta.in_comment(pos) {
return Some(non_code_meta); return true;
} }
} }
} }
None false
} }
// Return all the lsp folding ranges in the program. // Return all the lsp folding ranges in the program.
@ -1112,6 +1126,15 @@ impl NonCodeNode {
NonCodeValue::NewLine => "\n\n".to_string(), NonCodeValue::NewLine => "\n\n".to_string(),
} }
} }
fn is_comment(&self) -> bool {
matches!(
self.value,
NonCodeValue::InlineComment { .. }
| NonCodeValue::BlockComment { .. }
| NonCodeValue::NewLineBlockComment { .. }
)
}
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1189,14 +1212,22 @@ impl NonCodeMeta {
self.non_code_nodes.entry(i).or_default().push(new); self.non_code_nodes.entry(i).or_default().push(new);
} }
pub fn contains(&self, pos: usize) -> bool { pub fn in_comment(&self, pos: usize) -> bool {
if self.start_nodes.iter().any(|node| node.contains(pos)) { if self
.start_nodes
.iter()
.filter(|node| node.is_comment())
.any(|node| node.contains(pos))
{
return true; return true;
} }
self.non_code_nodes self.non_code_nodes.iter().any(|(_, nodes)| {
.iter() nodes
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos))) .iter()
.filter(|node| node.is_comment())
.any(|node| node.contains(pos))
})
} }
/// Get the non-code meta immediately before the ith node in the AST that self is attached to. /// Get the non-code meta immediately before the ith node in the AST that self is attached to.
@ -3381,7 +3412,7 @@ fn ghi = (x) => {
} }
#[test] #[test]
fn test_ast_get_non_code_node() { fn test_ast_in_comment() {
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3) let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
const h = 30 const h = 30
@ -3397,13 +3428,11 @@ const cylinder = startSketchOn('-XZ')
"#; "#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap(); let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(50); assert!(program.in_comment(50));
assert!(value.is_some());
} }
#[test] #[test]
fn test_ast_get_non_code_node_pipe() { fn test_ast_in_comment_pipe() {
let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3) let some_program_string = r#"const r = 20 / pow(pi(), 1 / 3)
const h = 30 const h = 30
@ -3420,22 +3449,18 @@ const cylinder = startSketchOn('-XZ')
"#; "#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap(); let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(124); assert!(program.in_comment(124));
assert!(value.is_some());
} }
#[test] #[test]
fn test_ast_get_non_code_node_inline_comment() { fn test_ast_in_comment_inline() {
let some_program_string = r#"const part001 = startSketchOn('XY') let some_program_string = r#"const part001 = startSketchOn('XY')
|> startProfileAt([0,0], %) |> startProfileAt([0,0], %)
|> xLine(5, %) // lin |> xLine(5, %) // lin
"#; "#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap(); let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(86); assert!(program.in_comment(86));
assert!(value.is_some());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -481,7 +481,6 @@ const jsAppSettings = async () => {
} }
const errFromErrWithOutputs = (e: any): KCLError => { const errFromErrWithOutputs = (e: any): KCLError => {
console.log(e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString()) const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
return new KCLError( return new KCLError(
parsed.error.kind, parsed.error.kind,