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)];
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 {
match attr.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);
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.
return Ok(None);
}
// 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)));
};

View File

@ -644,7 +644,10 @@ async fn test_kcl_lsp_completions() {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
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"#
.to_string(),
},
@ -658,7 +661,7 @@ st"#
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
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,
partial_result_params: Default::default(),
@ -671,6 +674,7 @@ st"#
// Check the completions.
if let tower_lsp::lsp_types::CompletionResponse::Array(completions) = completions {
assert!(completions.len() > 10);
assert!(completions.iter().any(|c| c.label == "@settings"));
} else {
panic!("Expected array of completions");
}

View File

@ -190,7 +190,7 @@ pub struct Program {
impl Node<Program> {
/// 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![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
let mut findings = completions.borrow_mut();
@ -208,8 +208,20 @@ impl Node<Program> {
}
Ok::<bool, anyhow::Error>(true)
})?;
let x = completions.take();
Ok(x.clone())
let mut completions = completions.take();
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.
@ -347,32 +359,34 @@ impl Program {
}
}
/// Returns a non code meta that includes the given character position.
pub fn get_non_code_meta_for_position(&self, pos: usize) -> Option<&NonCodeMeta> {
pub fn in_comment(&self, pos: usize) -> bool {
// Check if its in the body.
if self.non_code_meta.contains(pos) {
return Some(&self.non_code_meta);
if self.non_code_meta.in_comment(pos) {
return true;
}
let item = self.get_body_item_for_position(pos)?;
let item = self.get_body_item_for_position(pos);
// Recurse over the item.
let expr = match item {
BodyItem::ImportStatement(_) => None,
BodyItem::ExpressionStatement(expression_statement) => Some(&expression_statement.expression),
BodyItem::VariableDeclaration(variable_declaration) => variable_declaration.get_expr_for_position(pos),
BodyItem::ReturnStatement(return_statement) => Some(&return_statement.argument),
Some(BodyItem::ImportStatement(_)) => None,
Some(BodyItem::ExpressionStatement(expression_statement)) => Some(&expression_statement.expression),
Some(BodyItem::VariableDeclaration(variable_declaration)) => {
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.
if let Some(expr) = expr {
if let Some(non_code_meta) = expr.get_non_code_meta() {
if non_code_meta.contains(pos) {
return Some(non_code_meta);
if non_code_meta.in_comment(pos) {
return true;
}
}
}
None
false
}
// Return all the lsp folding ranges in the program.
@ -1112,6 +1126,15 @@ impl NonCodeNode {
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)]
@ -1189,14 +1212,22 @@ impl NonCodeMeta {
self.non_code_nodes.entry(i).or_default().push(new);
}
pub fn contains(&self, pos: usize) -> bool {
if self.start_nodes.iter().any(|node| node.contains(pos)) {
pub fn in_comment(&self, pos: usize) -> bool {
if self
.start_nodes
.iter()
.filter(|node| node.is_comment())
.any(|node| node.contains(pos))
{
return true;
}
self.non_code_nodes
.iter()
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
self.non_code_nodes.iter().any(|(_, nodes)| {
nodes
.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.
@ -3381,7 +3412,7 @@ fn ghi = (x) => {
}
#[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)
const h = 30
@ -3397,13 +3428,11 @@ const cylinder = startSketchOn('-XZ')
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(50);
assert!(value.is_some());
assert!(program.in_comment(50));
}
#[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)
const h = 30
@ -3420,22 +3449,18 @@ const cylinder = startSketchOn('-XZ')
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(124);
assert!(value.is_some());
assert!(program.in_comment(124));
}
#[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')
|> startProfileAt([0,0], %)
|> xLine(5, %) // lin
"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let value = program.get_non_code_meta_for_position(86);
assert!(value.is_some());
assert!(program.in_comment(86));
}
#[tokio::test(flavor = "multi_thread")]

View File

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