Code completions for @settings (#5622)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
@ -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),
|
||||
|
@ -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)));
|
||||
};
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user