diff --git a/kclvm/Cargo.lock b/kclvm/Cargo.lock index 5804760e2..504aca9db 100644 --- a/kclvm/Cargo.lock +++ b/kclvm/Cargo.lock @@ -1411,6 +1411,7 @@ dependencies = [ "kclvm-driver", "kclvm-error", "kclvm-parser", + "kclvm-query", "kclvm-sema", "kclvm-tools", "kclvm-utils", diff --git a/kclvm/ast/src/ast.rs b/kclvm/ast/src/ast.rs index 9bc80bd7a..28b3b2f9c 100644 --- a/kclvm/ast/src/ast.rs +++ b/kclvm/ast/src/ast.rs @@ -304,6 +304,14 @@ pub enum OverrideAction { CreateOrUpdate, } +/// KCL API symbol selector Spec, eg: `pkgpath:path.to.field` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct SymbolSelectorSpec { + pub pkg_root: String, + pub pkgpath: String, + pub field_path: String, +} + /// Program is the AST collection of all files of the running KCL program. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] pub struct Program { diff --git a/kclvm/query/src/lib.rs b/kclvm/query/src/lib.rs index 5d40a8657..31dabd359 100644 --- a/kclvm/query/src/lib.rs +++ b/kclvm/query/src/lib.rs @@ -5,6 +5,7 @@ //! AST, recursively modifying or deleting the values of the nodes in the AST. pub mod r#override; pub mod query; +pub mod selector; #[cfg(test)] mod tests; diff --git a/kclvm/query/src/selector.rs b/kclvm/query/src/selector.rs new file mode 100644 index 000000000..1d8d21497 --- /dev/null +++ b/kclvm/query/src/selector.rs @@ -0,0 +1,37 @@ +use super::util::{invalid_symbol_selector_spec_error, split_field_path}; +use anyhow::Result; +use kclvm_ast::ast; + +/// Parse symbol selector string to symbol selector spec +/// +/// # Examples +/// +/// ``` +/// use kclvm_query::selector::parse_symbol_selector_spec; +/// +/// if let Ok(spec) = parse_symbol_selector_spec("", "alice.age") { +/// assert_eq!(spec.pkgpath, "".to_string()); +/// assert_eq!(spec.field_path, "alice.age".to_string()); +/// } +/// ``` +pub fn parse_symbol_selector_spec( + pkg_root: &str, + symbol_path: &str, +) -> Result { + if let Ok((pkgpath, field_path)) = split_field_path(symbol_path) { + Ok(ast::SymbolSelectorSpec { + pkg_root: pkg_root.to_string(), + pkgpath, + field_path, + }) + } else { + Err(invalid_symbol_selector_spec_error(symbol_path)) + } +} + +#[test] +fn test_symbol_path_selector() { + let spec = parse_symbol_selector_spec("", "pkg_name:alice.age").unwrap(); + assert_eq!(spec.pkgpath, "pkg_name".to_string()); + assert_eq!(spec.field_path, "alice.age".to_string()); +} diff --git a/kclvm/query/src/util.rs b/kclvm/query/src/util.rs index 3d7bf3b90..f4d811fd9 100644 --- a/kclvm/query/src/util.rs +++ b/kclvm/query/src/util.rs @@ -27,3 +27,12 @@ pub(crate) fn split_field_path(path: &str) -> Result<(String, String)> { pub(crate) fn invalid_spec_error(spec: &str) -> anyhow::Error { anyhow!("Invalid spec format '{}', expected := or :-", spec) } + +/// Get the invalid symbol selector spec error message. +#[inline] +pub(crate) fn invalid_symbol_selector_spec_error(spec: &str) -> anyhow::Error { + anyhow!( + "Invalid spec format '{}', expected :", + spec + ) +} diff --git a/kclvm/spec/gpyrpc/gpyrpc.proto b/kclvm/spec/gpyrpc/gpyrpc.proto index c30469d3f..12c94ddd6 100644 --- a/kclvm/spec/gpyrpc/gpyrpc.proto +++ b/kclvm/spec/gpyrpc/gpyrpc.proto @@ -302,7 +302,7 @@ message KeyValuePair { message Rename_Args { string symbol_path = 1; // the path to the target symbol to be renamed. The symbol path should conform to format: `:` When the pkgpath is '__main__', `:` can be omitted. repeated string file_paths = 2; // the paths to the source code files - string newName = 3; // the new name of the symbol + string new_name = 3; // the new name of the symbol } message Rename_Result { @@ -317,7 +317,7 @@ message Rename_Result { message RenameCode_Args { string symbol_path = 1; // the path to the target symbol to be renamed. The symbol path should conform to format: `:` When the pkgpath is '__main__', `:` can be omitted. map source_codes = 2; // the source code. a : map - string newName = 3; // the new name of the symbol + string new_name = 3; // the new name of the symbol } message RenameCode_Result { diff --git a/kclvm/tools/src/LSP/Cargo.toml b/kclvm/tools/src/LSP/Cargo.toml index b6f612da4..6475aefa2 100644 --- a/kclvm/tools/src/LSP/Cargo.toml +++ b/kclvm/tools/src/LSP/Cargo.toml @@ -30,6 +30,7 @@ kclvm-ast = { path = "../../../ast" } kclvm-utils = { path = "../../../utils" } kclvm-version = { path = "../../../version" } compiler_base_session = { path = "../../../../compiler_base/session" } +kclvm-query = {path = "../../../query"} lsp-server = { version = "0.6.0", default-features = false } anyhow = { version = "1.0", default-features = false, features = ["std"] } diff --git a/kclvm/tools/src/LSP/src/lib.rs b/kclvm/tools/src/LSP/src/lib.rs index f773be282..47d36eb6b 100644 --- a/kclvm/tools/src/LSP/src/lib.rs +++ b/kclvm/tools/src/LSP/src/lib.rs @@ -13,6 +13,7 @@ mod hover; mod main_loop; mod notification; mod quick_fix; +mod rename; mod request; mod state; #[cfg(test)] diff --git a/kclvm/tools/src/LSP/src/rename.rs b/kclvm/tools/src/LSP/src/rename.rs new file mode 100644 index 000000000..978faf044 --- /dev/null +++ b/kclvm/tools/src/LSP/src/rename.rs @@ -0,0 +1,302 @@ +use crate::{ + from_lsp::kcl_pos, + goto_def::find_def_with_gs, + util::{build_word_index_for_file_paths, parse_param_and_compile, Param}, +}; +use chumsky::chain::Chain; +use kclvm_ast::{ast, token::LitKind::Err}; +use kclvm_query::selector::parse_symbol_selector_spec; +use kclvm_sema::core::symbol::{Symbol, SymbolKind, SymbolRef}; +use kclvm_sema::resolver::doc::Attribute; +use lsp_types::{Location, TextEdit, Url}; +use std::path::PathBuf; +use std::{collections::HashMap, ops::Deref}; + +/// the rename_symbol API +/// find all the occurrences of the target symbol and return the text edit actions to rename them +/// pkg_root: the absolute file path to the root package +/// file_paths: list of files in which symbols can be renamed +/// symbol_path: path to the symbol. The symbol path should be in the format of: `pkg.sub_pkg:name.sub_name` +/// new_name: the new name of the symbol +pub fn rename_symbol( + pkg_root: &str, + file_paths: Vec, + symbol_path: &str, + new_name: String, +) -> Result>, String> { + // 1. from symbol path to the symbol + match parse_symbol_selector_spec(pkg_root, symbol_path) { + Ok(symbol_spec) => { + if let Some((name, def)) = select_symbol(&symbol_spec) { + if def.is_none() { + return Result::Err(format!( + "can not find definition for symbol {}", + symbol_path + )); + } + match def.unwrap().get_kind() { + SymbolKind::Unresolved => { + return Result::Err(format!( + "can not resolve target symbol {}", + symbol_path + )); + } + _ => { + // 3. build word index on file_paths, find refs within file_paths scope + if let Ok(word_index) = build_word_index_for_file_paths(file_paths) { + if let Some(locations) = word_index.get(name.as_str()) { + // 4. filter out the matched refs + // 4.1 collect matched words(names) and remove Duplicates of the file paths + let file_map = locations.iter().fold( + HashMap::>::new(), + |mut acc, loc| { + acc.entry(loc.uri.clone()).or_insert(Vec::new()).push(loc); + acc + }, + ); + let refs = file_map + .iter() + .flat_map(|(_, locs)| locs.iter()) + .filter(|&&loc| { + // 4.2 filter out the words and remain those whose definition is the target def + let p = loc.uri.path(); + if let Ok((_, _, _, gs)) = parse_param_and_compile( + Param { + file: p.to_string(), + }, + None, + ) { + let kcl_pos = kcl_pos(p, loc.range.start); + if let Some(symbol_ref) = + find_def_with_gs(&kcl_pos, &gs, exact) + { + if let Some(real_def) = + gs.get_symbols().get_symbol(symbol_ref) + { + return real_def.get_definition() == def; + } + } + } + false + }) + .cloned() + .collect::>(); + // 5. refs to rename actions + let changes = + refs.into_iter().fold(HashMap::new(), |mut map, location| { + let uri = &location.uri; + map.entry(uri.clone()).or_insert_with(Vec::new).push( + TextEdit { + range: location.range, + new_text: new_name.clone(), + }, + ); + map + }); + return Ok(changes); + } + } + } + } + } + Result::Err("rename failed".to_string()) + } + Result::Err(err) => { + return Result::Err(format!( + "can not parse symbol path {}, {}", + symbol_path, err + )); + } + } +} + +/// Select a symbol by the symbol path +/// The symbol path should be in the format of: `pkg.sub_pkg:name.sub_name` +pub fn select_symbol(selector: &ast::SymbolSelectorSpec) -> Option<(String, Option)> { + let mut pkg = PathBuf::from(&selector.pkg_root); + let pkg_names = selector.pkgpath.split("."); + for n in pkg_names { + pkg = pkg.join(n) + } + + let fields: Vec<&str> = selector.field_path.split(".").collect(); + match pkg.as_path().to_str() { + Some(pkgpath) => { + // resolve pkgpath and get the symbol data by the fully qualified name + if let Ok((prog, _, _, gs)) = parse_param_and_compile( + Param { + file: pkgpath.to_string(), + }, + None, + ) { + if let Some(symbol_ref) = gs.get_symbols().get_symbol_by_fully_qualified_name( + format!("{}.{}", prog.main, fields[0]).as_str(), + ) { + let outer_symbol = gs.get_symbols().get_symbol(symbol_ref).unwrap(); + if fields.len() == 1 { + return Some((outer_symbol.get_name(), outer_symbol.get_definition())); + } + match symbol_ref.get_kind() { + SymbolKind::Schema => { + let schema = gs.get_symbols().get_schema_symbol(symbol_ref).unwrap(); + if let Some(attr) = + schema.get_attribute(fields[1], gs.get_symbols(), None) + { + let sym = gs.get_symbols().get_attribue_symbol(attr).unwrap(); + if fields.len() == 2 { + return Some((sym.get_name(), sym.get_definition())); + } + } + return None; + } + _ => { + // not supported by global state + return None; + } + } + } + } + None + } + None => None, + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{Location, Position, Range, Url}; + + use kclvm_ast::ast::{self, Pos}; + use std::collections::HashMap; + use std::fs::rename; + use std::path::PathBuf; + + use super::{rename_symbol, select_symbol}; + + #[test] + fn test_select_symbol() { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.push("src/test_data/rename_test/"); + let pkg_root = root.to_str().unwrap().to_string(); + + if let Some((name, Some(def))) = select_symbol(&ast::SymbolSelectorSpec { + pkg_root: pkg_root.clone(), + pkgpath: "base".to_string(), + field_path: "Person.name".to_string(), + }) { + assert_eq!(name, "name"); + assert_eq!( + def.get_kind(), + kclvm_sema::core::symbol::SymbolKind::Attribute + ); + } else { + assert!(false, "select symbol failed") + } + + if let Some((name, Some(def))) = select_symbol(&ast::SymbolSelectorSpec { + pkg_root: pkg_root.clone(), + pkgpath: "base".to_string(), + field_path: "Name.first".to_string(), + }) { + assert_eq!(name, "first"); + assert_eq!( + def.get_kind(), + kclvm_sema::core::symbol::SymbolKind::Attribute + ); + } else { + assert!(false, "select symbol failed") + } + + if let Some((name, Some(def))) = select_symbol(&ast::SymbolSelectorSpec { + pkg_root: pkg_root.clone(), + pkgpath: "base".to_string(), + field_path: "Person".to_string(), + }) { + assert_eq!(name, "Person"); + assert_eq!(def.get_kind(), kclvm_sema::core::symbol::SymbolKind::Schema); + } else { + assert!(false, "select symbol failed") + } + + if let Some((name, Some(def))) = select_symbol(&ast::SymbolSelectorSpec { + pkg_root: pkg_root.clone(), + pkgpath: "base".to_string(), + field_path: "a".to_string(), + }) { + assert_eq!(name, "a"); + assert_eq!(def.get_kind(), kclvm_sema::core::symbol::SymbolKind::Value); + } else { + assert!(false, "select symbol failed") + } + } + + #[test] + fn test_select_symbol_failed() { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.push("src/test_data/rename_test/"); + + let result = select_symbol(&ast::SymbolSelectorSpec { + pkg_root: root.to_str().unwrap().to_string(), + pkgpath: "base".to_string(), + field_path: "name".to_string(), + }); + assert!(result.is_none(), "should not find the target symbol") + } + + #[test] + fn test_rename() { + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + root.push("src/test_data/rename_test/"); + + let mut main_path = root.clone(); + let mut base_path = root.clone(); + base_path.push("base/person.k"); + main_path.push("config.k"); + + let base_url = Url::from_file_path(base_path.clone()).unwrap(); + let main_url = Url::from_file_path(main_path.clone()).unwrap(); + + if let Ok(changes) = rename_symbol( + root.to_str().unwrap(), + vec![ + base_path.to_str().unwrap().to_string(), + main_path.to_str().unwrap().to_string(), + ], + "base:Person", + "NewPerson".to_string(), + ) { + assert_eq!(changes.len(), 2); + assert!(changes.contains_key(&base_url)); + assert!(changes.contains_key(&main_url)); + assert!(changes.get(&base_url).unwrap().len() == 1); + assert!(changes.get(&base_url).unwrap()[0].range.start == Position::new(0, 7)); + assert!(changes.get(&main_url).unwrap().len() == 1); + assert!(changes.get(&main_url).unwrap()[0].range.start == Position::new(2, 9)); + assert!(changes.get(&main_url).unwrap()[0].new_text == "NewPerson".to_string()); + } else { + assert!(false, "rename failed") + } + + if let Ok(changes) = rename_symbol( + root.to_str().unwrap(), + vec![ + base_path.to_str().unwrap().to_string(), + main_path.to_str().unwrap().to_string(), + ], + "base:Person.name", + "new_name".to_string(), + ) { + println!("{:?}", changes); + assert_eq!(changes.len(), 2); + assert!(changes.contains_key(&base_url)); + assert!(changes.contains_key(&main_url)); + assert!(changes.get(&base_url).unwrap().len() == 1); + assert!(changes.get(&base_url).unwrap()[0].range.start == Position::new(1, 4)); + assert!(changes.get(&main_url).unwrap().len() == 1); + assert!(changes.get(&main_url).unwrap()[0].range.start == Position::new(4, 4)); + assert!(changes.get(&main_url).unwrap()[0].new_text == "new_name".to_string()); + } else { + assert!(false, "rename failed") + } + } +} diff --git a/kclvm/tools/src/LSP/src/test_data/rename_test/base/person.k b/kclvm/tools/src/LSP/src/test_data/rename_test/base/person.k new file mode 100644 index 000000000..781b5249f --- /dev/null +++ b/kclvm/tools/src/LSP/src/test_data/rename_test/base/person.k @@ -0,0 +1,13 @@ +schema Person: + name: Name + age: int + +schema Name: + first: str + +a = { + abc: "d" +} + +d = a.abc +e = a["abc"] \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/test_data/rename_test/config.k b/kclvm/tools/src/LSP/src/test_data/rename_test/config.k new file mode 100644 index 000000000..746f34df6 --- /dev/null +++ b/kclvm/tools/src/LSP/src/test_data/rename_test/config.k @@ -0,0 +1,8 @@ +import .base + +a = base.Person { + age: 1, + name: { + first: "aa" + } +} \ No newline at end of file diff --git a/kclvm/tools/src/LSP/src/util.rs b/kclvm/tools/src/LSP/src/util.rs index 3fab6c3cb..9ae8314ea 100644 --- a/kclvm/tools/src/LSP/src/util.rs +++ b/kclvm/tools/src/LSP/src/util.rs @@ -802,25 +802,32 @@ pub(crate) fn get_pkg_scope( .clone() } -/// scan and build a word -> Locations index map -pub fn build_word_index(path: String) -> anyhow::Result>> { +pub(crate) fn build_word_index_for_file_paths( + paths: Vec, +) -> anyhow::Result>> { let mut index: HashMap> = HashMap::new(); - if let Ok(files) = get_kcl_files(path.clone(), true) { - for file_path in &files { - // str path to url - if let Ok(url) = Url::from_file_path(file_path) { - // read file content and save the word to word index - let text = read_file(file_path)?; - for (key, values) in build_word_index_for_file_content(text, &url) { - index.entry(key).or_insert_with(Vec::new).extend(values); - } + for p in &paths { + // str path to url + if let Ok(url) = Url::from_file_path(p) { + // read file content and save the word to word index + let text = read_file(p)?; + for (key, values) in build_word_index_for_file_content(text, &url) { + index.entry(key).or_insert_with(Vec::new).extend(values); } } } return Ok(index); } -pub fn build_word_index_for_file_content( +/// scan and build a word -> Locations index map +pub(crate) fn build_word_index(path: String) -> anyhow::Result>> { + if let Ok(files) = get_kcl_files(path.clone(), true) { + return build_word_index_for_file_paths(files); + } + Ok(HashMap::new()) +} + +pub(crate) fn build_word_index_for_file_content( content: String, url: &Url, ) -> HashMap> { @@ -844,7 +851,7 @@ pub fn build_word_index_for_file_content( index } -pub fn word_index_add( +pub(crate) fn word_index_add( from: &mut HashMap>, add: HashMap>, ) { @@ -853,7 +860,7 @@ pub fn word_index_add( } } -pub fn word_index_subtract( +pub(crate) fn word_index_subtract( from: &mut HashMap>, remove: HashMap>, ) {