From 6aef1a29e3f0fd3f3a2178557a758bfa2e792178 Mon Sep 17 00:00:00 2001 From: He1pa <56333845+He1pa@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:17:44 +0800 Subject: [PATCH] feat: completion for schema attr value. (#809) feat: completion for schema attr value. Completion when input = or : in config schema attr, and completion item contains primitiver type, literal, schema, list, dict, union Signed-off-by: He1pa <18012015693@163.com> --- kclvm/tools/src/LSP/src/capabilities.rs | 6 +- kclvm/tools/src/LSP/src/completion.rs | 245 ++++++++++++++++-- .../completion_test/assign/completion.k | 27 ++ .../completion_test/assign/pkg/file1.k | 0 .../completion_test/assign/pkg/file2.k | 0 .../completion_test/assign/pkg/subpkg/file1.k | 3 + 6 files changed, 260 insertions(+), 21 deletions(-) create mode 100644 kclvm/tools/src/LSP/src/test_data/completion_test/assign/completion.k create mode 100644 kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file1.k create mode 100644 kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file2.k create mode 100644 kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/subpkg/file1.k diff --git a/kclvm/tools/src/LSP/src/capabilities.rs b/kclvm/tools/src/LSP/src/capabilities.rs index 5f633b255..2b53d63f5 100644 --- a/kclvm/tools/src/LSP/src/capabilities.rs +++ b/kclvm/tools/src/LSP/src/capabilities.rs @@ -12,7 +12,11 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti document_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions { resolve_provider: None, - trigger_characters: Some(vec![String::from(".")]), + trigger_characters: Some(vec![ + String::from("."), + String::from("="), + String::from(":"), + ]), all_commit_characters: None, work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, diff --git a/kclvm/tools/src/LSP/src/completion.rs b/kclvm/tools/src/LSP/src/completion.rs index b74cbc2d5..ad605badf 100644 --- a/kclvm/tools/src/LSP/src/completion.rs +++ b/kclvm/tools/src/LSP/src/completion.rs @@ -1,19 +1,26 @@ //! Complete for KCL -//! Github Issue: https://github.com/kcl-lang/kcl/issues/476 -//! Now supports code completion in treigger mode (triggered when user enters `.`), +//! Now supports code completion in treigger mode (triggered when user enters `.`, `:` and `=`), schema attr and global variables //! and the content of the completion includes: -//! + import path -//! + schema attr -//! + builtin function(str function) -//! + defitions in pkg -//! + system module functions +//! variable +//! + schema attr name +//! + dot(.) +//! + import path +//! + schema attr +//! + builtin function(str function) +//! + defitions in pkg +//! + system module functions +//! + assign(=, :) +//! + schema attr value +//! + variable value use std::io; use std::{fs, path::Path}; +use chumsky::primitive::todo; use indexmap::IndexSet; use kclvm_ast::ast::{Expr, ImportStmt, Node, Program, Stmt}; use kclvm_ast::pos::GetPos; +use kclvm_ast::MAIN_PKG; use kclvm_compiler::pkgpath_without_prefix; use kclvm_config::modfile::KCL_FILE_EXTENSION; @@ -23,7 +30,7 @@ use kclvm_sema::builtin::{ STRING_MEMBER_FUNCTIONS, }; use kclvm_sema::resolver::scope::{ProgramScope, ScopeObjectKind}; -use kclvm_sema::ty::FunctionType; +use kclvm_sema::ty::{FunctionType, Type}; use lsp_types::{CompletionItem, CompletionItemKind}; use crate::goto_def::{find_def, get_identifier_last_name, Definition}; @@ -67,6 +74,41 @@ fn func_ty_complete_label(func_name: &String, func_type: &FunctionType) -> Strin ) } +fn ty_complete_label(ty: &Type) -> Vec { + match &ty.kind { + kclvm_sema::ty::TypeKind::Bool => vec!["True".to_string(), "False".to_string()], + kclvm_sema::ty::TypeKind::BoolLit(b) => { + vec![if *b { + "True".to_string() + } else { + "False".to_string() + }] + } + kclvm_sema::ty::TypeKind::IntLit(i) => vec![i.to_string()], + kclvm_sema::ty::TypeKind::FloatLit(f) => vec![f.to_string()], + kclvm_sema::ty::TypeKind::Str => vec![r#""""#.to_string()], + kclvm_sema::ty::TypeKind::StrLit(s) => vec![format!("{:?}", s)], + kclvm_sema::ty::TypeKind::List(_) => vec!["[]".to_string()], + kclvm_sema::ty::TypeKind::Dict(_) => vec!["{}".to_string()], + kclvm_sema::ty::TypeKind::Union(types) => { + types.iter().flat_map(|ty| ty_complete_label(ty)).collect() + } + kclvm_sema::ty::TypeKind::Schema(schema) => { + vec![format!( + "{}{}{}", + if schema.pkgpath.is_empty() || schema.pkgpath == MAIN_PKG { + "".to_string() + } else { + format!("{}.", schema.pkgpath.split(".").last().unwrap()) + }, + schema.name, + "{}" + )] + } + _ => vec![], + } +} + /// Computes completions at the given position. pub(crate) fn completion( trigger_character: Option, @@ -74,23 +116,28 @@ pub(crate) fn completion( pos: &KCLPos, prog_scope: &ProgramScope, ) -> Option { - if let Some('.') = trigger_character { - completion_dot(program, pos, prog_scope) - } else { - let mut completions: IndexSet = IndexSet::new(); + match trigger_character { + Some(c) => match c { + '.' => completion_dot(program, pos, prog_scope), + '=' | ':' => completion_assign(program, pos, prog_scope), + _ => None, + }, + None => { + let mut completions: IndexSet = IndexSet::new(); - completions.extend(completion_variable(pos, prog_scope)); + completions.extend(completion_variable(pos, prog_scope)); - completions.extend(completion_attr(program, pos, prog_scope)); + completions.extend(completion_attr(program, pos, prog_scope)); - completions.extend(completion_import_builtin_pkg(program, pos, prog_scope)); + completions.extend(completion_import_builtin_pkg(program, pos, prog_scope)); - Some(into_completion_items(&completions).into()) + Some(into_completion_items(&completions).into()) + } } } /// Abstraction of CompletionItem in KCL -#[derive(Debug, Clone, PartialEq, Hash, Eq)] +#[derive(Debug, Clone, PartialEq, Hash, Eq, Default)] pub(crate) struct KCLCompletionItem { pub label: String, pub detail: Option, @@ -113,12 +160,32 @@ fn completion_dot( match program.pos_to_stmt(pos) { Some(node) => match node.node { Stmt::Import(stmt) => completion_for_import(&stmt, pos, prog_scope, program), - _ => Some(into_completion_items(&get_completion(node, pos, prog_scope)).into()), + _ => Some(into_completion_items(&get_doc_completion(node, pos, prog_scope)).into()), }, None => None, } } +fn completion_assign( + program: &Program, + pos: &KCLPos, + prog_scope: &ProgramScope, +) -> Option { + // Get the position of trigger_character '=' or ':' + let pos = &KCLPos { + filename: pos.filename.clone(), + line: pos.line, + column: pos.column.map(|c| c - 1), + }; + + match program.pos_to_stmt(pos) { + Some(node) => Some( + into_completion_items(&get_schema_attr_value_completion(node, pos, prog_scope)).into(), + ), + None => None, + } +} + fn completion_import_builtin_pkg( program: &Program, pos: &KCLPos, @@ -276,7 +343,45 @@ fn completion_for_import( Some(into_completion_items(&items).into()) } -pub(crate) fn get_completion( +/// Get completion items for trigger '=' or ':' +/// Now, just completion for schema attr value +pub(crate) fn get_schema_attr_value_completion( + stmt: Node, + pos: &KCLPos, + prog_scope: &ProgramScope, +) -> IndexSet { + let mut items: IndexSet = IndexSet::new(); + let (expr, _) = inner_most_expr_in_stmt(&stmt.node, pos, None); + if let Some(node) = expr { + if let Expr::Identifier(_) = node.node { + let def = find_def(stmt, pos, prog_scope); + if let Some(def) = def { + match def { + crate::goto_def::Definition::Object(obj, _) => match obj.kind { + ScopeObjectKind::Attribute => { + let ty = obj.ty; + items.extend(ty_complete_label(&ty).iter().map(|label| { + KCLCompletionItem { + label: format!(" {}", label), + detail: Some(format!("{}: {}", obj.name, ty.ty_str())), + kind: Some(KCLCompletionItemKind::Variable), + documentation: obj.doc.clone(), + } + })) + } + _ => {} + }, + _ => {} + } + } + } + } + + items +} + +/// Get completion items for trigger '.' +pub(crate) fn get_doc_completion( stmt: Node, pos: &KCLPos, prog_scope: &ProgramScope, @@ -399,7 +504,8 @@ pub(crate) fn get_completion( } } Expr::Selector(select_expr) => { - let res = get_completion(stmt, &select_expr.value.get_end_pos(), prog_scope); + let res = + get_doc_completion(stmt, &select_expr.value.get_end_pos(), prog_scope); items.extend(res); } Expr::StringLit(_) => { @@ -820,4 +926,103 @@ mod tests { let expect: CompletionResponse = into_completion_items(&items).into(); assert_eq!(got, expect); } + + #[test] + #[bench_test] + fn attr_value_completion() { + let (file, program, prog_scope, _) = + compile_test_file("src/test_data/completion_test/assign/completion.k"); + + let pos = KCLPos { + filename: file.to_owned(), + line: 14, + column: Some(6), + }; + + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" True", " False"]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 16, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" \"abc\"", " \"def\""]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 18, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" []"]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 20, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" 1"]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 22, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" True"]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 24, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" {}"]; + assert_eq!(got_labels, expected_labels); + + let pos = KCLPos { + filename: file.to_owned(), + line: 26, + column: Some(6), + }; + let got = completion(Some(':'), &program, &pos, &prog_scope).unwrap(); + let got_labels: Vec = match got { + CompletionResponse::Array(arr) => arr.iter().map(|item| item.label.clone()).collect(), + CompletionResponse::List(_) => panic!("test failed"), + }; + let expected_labels: Vec<&str> = vec![" subpkg.Person1{}"]; + assert_eq!(got_labels, expected_labels); + } } diff --git a/kclvm/tools/src/LSP/src/test_data/completion_test/assign/completion.k b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/completion.k new file mode 100644 index 000000000..d89b76515 --- /dev/null +++ b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/completion.k @@ -0,0 +1,27 @@ +import .pkg +import .pkg.subpkg + +schema Person2: + a: bool + b: "abc" | "def" + c: [int] + d: 1 + e: True + f: {str:str} + g: subpkg.Person1 + +p5 = Person2{ + a # complete `True` and `False` + + b # complete `"abc"` and `"def"` + + c # complete `[]` + + d # complete `1` + + e # complete `True` + + f # complete `{}` + + g # complete `subpkg.Person1{}` +} diff --git a/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file1.k b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file1.k new file mode 100644 index 000000000..e69de29bb diff --git a/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file2.k b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/file2.k new file mode 100644 index 000000000..e69de29bb diff --git a/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/subpkg/file1.k b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/subpkg/file1.k new file mode 100644 index 000000000..3ab0802b2 --- /dev/null +++ b/kclvm/tools/src/LSP/src/test_data/completion_test/assign/pkg/subpkg/file1.k @@ -0,0 +1,3 @@ +schema Person1: + name: str + age: int \ No newline at end of file