From b858abc3cbeb492cbc2a2008f1c2e535b1f38cae Mon Sep 17 00:00:00 2001 From: fda-odoo Date: Thu, 9 Oct 2025 11:39:06 +0200 Subject: [PATCH] [IMP] core: autocompletion for import statements --- server/src/core/import_resolver.rs | 197 ++++++++++++++++++++++++----- server/src/features/completion.rs | 33 ++--- 2 files changed, 183 insertions(+), 47 deletions(-) diff --git a/server/src/core/import_resolver.rs b/server/src/core/import_resolver.rs index 7b08d0f7..3c5580a0 100644 --- a/server/src/core/import_resolver.rs +++ b/server/src/core/import_resolver.rs @@ -1,6 +1,8 @@ use glob::glob; -use lsp_types::{Diagnostic, DiagnosticTag, Position, Range}; +use itertools::Itertools; +use lsp_types::{CompletionItemKind, Diagnostic, DiagnosticTag, Position, Range}; use ruff_python_ast::name::Name; +use serde::Serialize; use tracing::error; use std::collections::{HashMap, HashSet}; use std::rc::Rc; @@ -263,6 +265,9 @@ fn _get_or_create_symbol(session: &mut SessionInfo, for_entry: &Rc>> = symbol.clone(); let mut last_symbol = symbol.clone(); for branch in names.iter() { + if branch.is_empty() { + continue; + } match sym { Some(ref s) => { let mut next_symbol = s.borrow().get_symbol(&(vec![branch.clone()], vec![]), u32::MAX); @@ -359,6 +364,9 @@ fn _get_or_create_symbol(session: &mut SessionInfo, for_entry: &Rc>, name: &OYarn, asname: Option) -> Result>, String> { + if name == "" { + return Err("Empty name".to_string()); + } if DEBUG_BORROW_GUARDS { //Parent must be borrowable in this function parent.borrow_mut(); @@ -434,64 +442,189 @@ fn _resolve_new_symbol(session: &mut SessionInfo, parent: Rc>, n return Err("Symbol not found".to_string()) } -pub fn get_all_valid_names(session: &mut SessionInfo, source_file_symbol: &Rc>, from_stmt: Option<&Identifier>, base_name: String, level: Option) -> HashSet { - //A: search base of different imports +/* +Used for autocompletion. Given a base_name, return all valid names that can be used to complete it. +is_from indicates if the import is the X in "from X import Y". Else it is Y from "import Y" or "from X import Y" +*/ +pub fn get_all_valid_names(session: &mut SessionInfo, source_file_symbol: &Rc>, from_stmt: Option, import: String, level: Option, is_from: bool) -> HashMap { + let (identifier_from, to_complete) = match from_stmt { + Some(from_stmt_inner) => { + if is_from { + let split = from_stmt_inner.split(".").collect::>(); + if split.len() > 1 { + (Some(Identifier::new(split[0..split.len()-1].join(".").as_str(), TextRange::default())), split.last().unwrap().to_string()) + } else { + (None, split.last().unwrap().to_string()) + } + } else { + (Some(Identifier::new(from_stmt_inner.clone(), TextRange::default())), import.clone()) + } + }, + None => (None, import.split(".").last().unwrap().to_string()), + }; + //A: Search base to search on let source_root = source_file_symbol.borrow().get_root().as_ref().unwrap().upgrade().unwrap(); let entry = source_root.borrow().get_entry().unwrap(); let _source_file_symbol_lock = source_file_symbol.borrow_mut(); let file_tree = _resolve_packages( &_source_file_symbol_lock, level, - from_stmt); + identifier_from.as_ref()); drop(_source_file_symbol_lock); let mut start_symbol = None; - if level.is_some() { - //if level is some, resolve_pacackages already built a full tree, so we can start from root + let source_path = source_file_symbol.borrow().paths()[0].clone(); + if !file_tree.is_empty() && level.is_some() && level.unwrap() != 0 { start_symbol = Some(source_root.clone()); } - let source_path = source_file_symbol.borrow().paths()[0].clone(); - let (from_symbol, _fallback_sym) = _get_or_create_symbol(session, + let (mut from_symbol, _fallback_sym) = _get_or_create_symbol(session, &entry, source_path.as_str(), start_symbol, &file_tree, None, level); - let mut result = HashSet::new(); + let mut result = HashMap::new(); + let mut symbols_to_browse = vec![]; if from_symbol.is_none() { + if !file_tree.is_empty() { //symbol was not found + return result; + } else { //nothing was provided, so we have to add the root symbol of any valid entrypoint + let entry_point_mgr = session.sync_odoo.entry_point_mgr.clone(); + let entry_point_mgr = entry_point_mgr.borrow(); + let from_path = session.sync_odoo.entry_point_mgr.borrow().transform_addon_path(&PathBuf::from(source_path.clone())); + let from_path = PathBuf::from(from_path); + for entry in entry_point_mgr.iter_for_import(&entry) { + if (entry.borrow().is_public() && (level.is_none() || level.unwrap() == 0)) || entry.borrow().is_valid_for(&from_path) { + let entry_point = entry.borrow().get_symbol(); + if let Some(entry_point) = entry_point { + symbols_to_browse.push(entry_point.clone()); + } + } + } + if symbols_to_browse.is_empty() { + return result; + } + } + } + if is_from { + if let Some(fs) = from_symbol { + symbols_to_browse.push(fs); + } + for symbol_to_browse in symbols_to_browse.iter() { + let valid_names = valid_names_for_a_symbol(session, symbol_to_browse, &oyarn!("{}", to_complete), true); + result.extend(valid_names); + } return result; } - let from_symbol = from_symbol.unwrap(); - let mut sym: Option>> = Some(from_symbol.clone()); - let mut names = vec![base_name.split(".").map(|s| oyarn!("{}", s)).next().unwrap()]; - if base_name.ends_with(".") { - names.push(Sy!("")); + let import_parts = import.split(".").collect::>(); + if import_parts.len() > 1 { + let (next_symbol, _fallback_sym) = _get_or_create_symbol( + session, + &entry, + source_path.as_str(), + from_symbol.clone(), + &import_parts[0..import_parts.len()-1].iter().map(|s| oyarn!("{}", *s)).collect(), + None, + level, + ); + if next_symbol.is_none() { + return result; + } + from_symbol = next_symbol.clone(); } - for (index, branch) in names.iter().enumerate() { - if index != names.len() -1 { - let mut next_symbol = sym.as_ref().unwrap().borrow().get_symbol(&(vec![branch.clone()], vec![]), u32::MAX); - if next_symbol.is_empty() { - next_symbol = match _resolve_new_symbol(session, sym.as_ref().unwrap().clone(), &branch, None) { - Ok(v) => vec![v], - Err(_) => vec![] - } + if let Some(fs) = from_symbol { + symbols_to_browse.clear(); + symbols_to_browse.push(fs); + } + for symbol_to_browse in symbols_to_browse.iter() { + let valid_names = valid_names_for_a_symbol(session, symbol_to_browse, &oyarn!("{}", to_complete), false); + result.extend(valid_names); + } + result +} + +fn valid_names_for_a_symbol(session: &mut SessionInfo, symbol: &Rc>, start_filter: &OYarn, only_on_disk: bool) -> HashMap { + let mut res = HashMap::new(); + match symbol.borrow().typ() { + SymType::FILE => { + if only_on_disk { + return res; } - if next_symbol.is_empty() { - sym = None; - break; + res.extend(valid_name_from_symbol(symbol, start_filter)); + }, + SymType::NAMESPACE | SymType::DISK_DIR => { + for path in symbol.borrow().paths().iter() { + res.extend(valid_name_from_disk(path, start_filter)); + } + }, + SymType::PACKAGE(_) => { + for path in symbol.borrow().paths().iter() { + res.extend(valid_name_from_disk(path, start_filter)); } - sym = Some(next_symbol[0].clone()); + if only_on_disk { + return res; + } + res.extend(valid_name_from_symbol(symbol, start_filter)); + } + SymType::CLASS | SymType::COMPILED | SymType::CSV_FILE | SymType::XML_FILE | SymType::FUNCTION | SymType::ROOT | SymType::VARIABLE => { } } - if let Some(sym) = sym { - let filter = names.last().unwrap(); - for symbol in sym.borrow().all_symbols() { - if symbol.borrow().name().starts_with(filter.as_str()) { - result.insert(symbol.borrow().name().clone()); + res +} + +fn valid_name_from_disk(path: &String, start_filter: &OYarn) -> HashMap { + let mut res = HashMap::new(); + if is_dir_cs(path.clone()) { + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries { + if let Ok(entry) = entry { + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_dir() { + let dir_name = entry.file_name(); + let dir_name_str = dir_name.to_string_lossy(); + if dir_name_str.starts_with(start_filter.as_str()) { + let mut typ = SymType::NAMESPACE; + if Path::new(&path).join(dir_name_str.to_string()).join("__init__.py").exists() { + typ = SymType::PACKAGE(PackageType::PYTHON_PACKAGE); + } + res.insert(Sy!(dir_name_str.to_string()), typ); + } + } else if file_type.is_file() { + let file_name = entry.file_name(); + let file_name_str = file_name.to_string_lossy().to_string(); + if (file_name_str.ends_with(".py") || file_name_str.ends_with(".pyi")) && file_name_str.starts_with(start_filter.as_str()) { + let Some(stem) = Path::new(&file_name_str).file_stem() else {continue}; + let Some(filename) = stem.to_str() else {continue}; + if filename == "__init__" {continue;} + res.insert(Sy!(filename.to_string()), SymType::FILE); + } + } + //TODO support for symlinks? + } } } } + res +} - return result; +fn valid_name_from_symbol(symbol: &Rc>, start_filter: &OYarn) -> HashMap { + let mut res = HashMap::new(); + let symbols = symbol.borrow(); + for s in symbols.iter_symbols() { + if s.0.starts_with(&start_filter.to_string()) { + let mut typ = SymType::VARIABLE; + let a_section = s.1.iter().last(); //let's take the last section, anyway we can display only one icon + if let Some(a_section) = a_section { + let last = a_section.1.last(); + if let Some(last) = last { + typ = last.borrow().typ(); + } + } + res.insert(s.0.clone(), typ); + } + } + res } diff --git a/server/src/features/completion.rs b/server/src/features/completion.rs index 23649e38..3a9b7d13 100644 --- a/server/src/features/completion.rs +++ b/server/src/features/completion.rs @@ -308,12 +308,13 @@ fn complete_assert_stmt(session: &mut SessionInfo<'_>, file: &Rc fn complete_import_stmt(session: &mut SessionInfo, file: &Rc>, stmt_import: &StmtImport, offset: usize) -> Option { let mut items = vec![]; for alias in stmt_import.names.iter() { - if alias.name.range().end().to_usize() == offset { - let names = import_resolver::get_all_valid_names(session, file, None, S!(alias.name.id.as_str()), None); - for name in names { + if alias.name.range().start().to_usize() < offset && alias.name.range.end().to_usize() >= offset { + let to_complete = alias.name.id.to_string().get(0 .. offset - alias.name.range.start().to_usize()).unwrap_or("").to_string(); + let names = import_resolver::get_all_valid_names(session, file, None, to_complete, None, false); + for (name, sym_typ) in names { items.push(CompletionItem { label: name.to_string(), - kind: Some(lsp_types::CompletionItemKind::MODULE), + kind: Some(get_completion_item_kind(&sym_typ)), ..Default::default() }); } @@ -328,24 +329,26 @@ fn complete_import_stmt(session: &mut SessionInfo, file: &Rc>, s fn complete_import_from_stmt(session: &mut SessionInfo, file: &Rc>, stmt_import: &StmtImportFrom, offset: usize) -> Option { let mut items = vec![]; if let Some(module) = stmt_import.module.as_ref() { - if module.range.end().to_usize() == offset && !stmt_import.names.is_empty() { - let names = import_resolver::get_all_valid_names(session, file, None, S!(stmt_import.names[0].name.id.as_str()), Some(stmt_import.level)); - for name in names { + if module.range.start().to_usize() < offset && module.range.end().to_usize() >= offset { + let to_complete = module.id.to_string().get(0 .. offset - module.range.start().to_usize()).unwrap_or("").to_string(); + let names = import_resolver::get_all_valid_names(session, file, Some(to_complete), S!(""), Some(stmt_import.level), true); + for (name, sym_type) in names { items.push(CompletionItem { label: name.to_string(), - kind: Some(lsp_types::CompletionItemKind::MODULE), + kind: Some(get_completion_item_kind(&sym_type)), ..Default::default() }); } } } for alias in stmt_import.names.iter() { - if alias.name.range().end().to_usize() == offset { - let names = import_resolver::get_all_valid_names(session, file, stmt_import.module.as_ref(), S!(alias.name.id.as_str()), Some(stmt_import.level)); - for name in names { + if alias.name.range().start().to_usize() < offset && alias.name.range.end().to_usize() >= offset { + let to_complete = alias.name.id.to_string().get(0 .. offset - alias.name.range.start().to_usize()).unwrap_or("").to_string(); + let names = import_resolver::get_all_valid_names(session, file, stmt_import.module.as_ref().map(|m| m.id.to_string()), to_complete, Some(stmt_import.level), false); + for (name, sym_type) in names { items.push(CompletionItem { label: name.to_string(), - kind: Some(lsp_types::CompletionItemKind::MODULE), + kind: Some(get_completion_item_kind(&sym_type)), ..Default::default() }); } @@ -1084,7 +1087,7 @@ fn build_completion_item_from_symbol(session: &mut SessionInfo, symbols: Vec>/*, cl: Option>) -> CompletionItemKind { - match symbol.borrow().typ() { +fn get_completion_item_kind(typ: &SymType) -> CompletionItemKind { + match typ { SymType::ROOT => CompletionItemKind::TEXT, SymType::DISK_DIR => CompletionItemKind::FOLDER, SymType::NAMESPACE => CompletionItemKind::FOLDER,