diff --git a/server/src/core/entry_point.rs b/server/src/core/entry_point.rs index f5c0f866..000fad3f 100644 --- a/server/src/core/entry_point.rs +++ b/server/src/core/entry_point.rs @@ -14,6 +14,7 @@ pub struct EntryPointMgr { pub main_entry_point: Option>>, pub addons_entry_points: Vec>>, pub custom_entry_points: Vec>>, + pub untitled_entry_points: Vec>>, } impl EntryPointMgr { @@ -25,8 +26,27 @@ impl EntryPointMgr { main_entry_point: None, addons_entry_points: vec![], custom_entry_points: vec![], + untitled_entry_points: vec![], } } + /// Create a new entry for an untitled (in-memory) file. + /// Returns the file symbol for the untitled entry. + pub fn add_entry_to_untitled(session: &mut SessionInfo, path: String) -> Rc> { + // For untitled files, we use a minimal tree: just the name as a single OYarn + let tree = vec![OYarn::from(path.clone())]; + let entry = EntryPoint::new( + path.clone(), + tree, + EntryPointType::UNTITLED, + None, + None, + ); + session.sync_odoo.entry_point_mgr.borrow_mut().untitled_entry_points.push(entry.clone()); + // Create one file symbol under the root for the untitled file + let name: String = PathBuf::from(&path).with_extension("").components().last().unwrap().as_os_str().to_str().unwrap().to_string(); + let file_sym = entry.borrow().root.borrow_mut().add_new_file(session, &name, &path); + file_sym.clone() + } /** * Create each required directory symbols for a given path. @@ -193,6 +213,13 @@ impl EntryPointMgr { true } + pub fn create_new_untitled_entry_for_path(session: &mut SessionInfo, file_name: &String) -> bool { + let new_sym = EntryPointMgr::add_entry_to_untitled(session, file_name.clone()); + new_sym.borrow_mut().as_file_mut().self_import = true; + SyncOdoo::add_to_rebuild_arch(session.sync_odoo, new_sym); + true + } + pub fn iter_for_import(&self, current_entry: &Rc>) -> Box>> + '_> { let mut is_main = false; for entry in self.iter_main() { @@ -214,12 +241,12 @@ impl EntryPointMgr { } pub fn iter_all(&self) -> impl Iterator>> { - self.addons_entry_points.iter().chain( - self.main_entry_point.iter()).chain( - self.builtins_entry_points.iter()).chain( - self.public_entry_points.iter()).chain( - self.custom_entry_points.iter() - ) + self.addons_entry_points.iter() + .chain(self.main_entry_point.iter()) + .chain(self.builtins_entry_points.iter()) + .chain(self.public_entry_points.iter()) + .chain(self.custom_entry_points.iter()) + .chain(self.untitled_entry_points.iter()) } //iter through all main entry points, sorted by tree length (from bigger to smaller) @@ -231,10 +258,10 @@ impl EntryPointMgr { } pub fn iter_all_but_main(&self) -> impl Iterator>> { - self.builtins_entry_points.iter().chain( - self.public_entry_points.iter()).chain( - self.custom_entry_points.iter() - ) + self.builtins_entry_points.iter() + .chain(self.public_entry_points.iter()) + .chain(self.custom_entry_points.iter()) + .chain(self.untitled_entry_points.iter()) } pub fn reset_entry_points(&mut self, with_custom_entries: bool) { @@ -249,7 +276,9 @@ impl EntryPointMgr { pub fn remove_entries_with_path(&mut self, path: &String) { for entry in self.iter_all() { - if PathBuf::from(entry.borrow().path.clone()).starts_with(path) { //delete any entrypoint that would be in a subdirectory too + if (entry.borrow().typ == EntryPointType::UNTITLED && entry.borrow().path == *path) + || (entry.borrow().typ != EntryPointType::UNTITLED + && PathBuf::from(entry.borrow().path.clone()).starts_with(path)){ //delete any entrypoint that would be in a subdirectory too entry.borrow_mut().to_delete = true; } } @@ -292,7 +321,7 @@ impl EntryPointMgr { entry_index += 1; } } - let mut entry_index = 0; + entry_index = 0; while entry_index < self.custom_entry_points.len() { let entry = self.custom_entry_points[entry_index].clone(); if entry.borrow().to_delete { @@ -302,6 +331,16 @@ impl EntryPointMgr { entry_index += 1; } } + entry_index = 0; + while entry_index < self.untitled_entry_points.len() { + let entry = self.untitled_entry_points[entry_index].clone(); + if entry.borrow().to_delete { + info!("Dropping untitled entry point {}", entry.borrow().path); + self.untitled_entry_points.remove(entry_index); + } else { + entry_index += 1; + } + } } /// Transform the path of an addon to the odoo relative path. @@ -323,7 +362,8 @@ pub enum EntryPointType { BUILTIN, PUBLIC, ADDON, - CUSTOM + CUSTOM, + UNTITLED, } #[derive(Debug, Clone)] @@ -359,6 +399,9 @@ impl EntryPoint { } pub fn is_valid_for(&self, path: &PathBuf) -> bool { + if self.typ == EntryPointType::UNTITLED { + return self.path == path.sanitize(); + } path.starts_with(&self.path) } diff --git a/server/src/core/file_mgr.rs b/server/src/core/file_mgr.rs index f9dd440e..1875076d 100644 --- a/server/src/core/file_mgr.rs +++ b/server/src/core/file_mgr.rs @@ -111,9 +111,9 @@ impl FileInfo { diagnostic_filters: Vec::new(), } } - pub fn update(&mut self, session: &mut SessionInfo, uri: &str, content: Option<&Vec>, version: Option, in_workspace: bool, force: bool) -> bool { + pub fn update(&mut self, session: &mut SessionInfo, path: &str, content: Option<&Vec>, version: Option, in_workspace: bool, force: bool, is_untitled: bool) -> bool { // update the file info with the given information. - // uri: indicates the path of the file + // path: indicates the path of the file // content: if content is given, it will be used to update the ast and text_rope, if not, the loading will be from the disk // version: if the version is provided, the file_info wil be updated only if the new version is higher. // -100 can be given as version number to indicates that the file has not been opened yet, and that we have to load it ourself @@ -144,13 +144,16 @@ impl FileInfo { for change in content.iter() { self.apply_change(change); } + } else if is_untitled { + session.log_message(MessageType::ERROR, format!("Attempt to update untitled file {}, without changes", path)); + return false; } else { - match fs::read_to_string(uri) { + match fs::read_to_string(path) { Ok(content) => { self.file_info_ast.borrow_mut().text_rope = Some(ropey::Rope::from(content.as_str())); }, Err(e) => { - session.log_message(MessageType::ERROR, format!("Failed to read file {}, with error {}", uri, e)); + session.log_message(MessageType::ERROR, format!("Failed to read file {}, with error {}", path, e)); return false; }, }; @@ -474,6 +477,7 @@ impl FileInfo { #[derive(Debug)] pub struct FileMgr { pub files: HashMap>>, + untitled_files: HashMap>>, // key: untitled URI or unique name workspace_folders: HashMap, has_repeated_workspace_folders: bool, } @@ -483,6 +487,7 @@ impl FileMgr { pub fn new() -> Self { Self { files: HashMap::new(), + untitled_files: HashMap::new(), workspace_folders: HashMap::new(), has_repeated_workspace_folders: false, } @@ -496,17 +501,30 @@ impl FileMgr { } pub fn get_file_info(&self, path: &String) -> Option>> { - self.files.get(path).cloned() + if Self::is_untitled(path) { + self.untitled_files.get(path).cloned() + } else { + self.files.get(path).cloned() + } } pub fn text_range_to_range(&self, session: &mut SessionInfo, path: &String, range: &TextRange) -> Range { - let file = self.files.get(path); + let file = if Self::is_untitled(path) { + self.untitled_files.get(path) + } else { + self.files.get(path) + }; if let Some(file) = file { if file.borrow().file_info_ast.borrow().text_rope.is_none() { file.borrow_mut().prepare_ast(session); } return file.borrow().text_range_to_range(range); } + // For untitled, never try to read from disk + if Self::is_untitled(path) { + session.log_message(MessageType::ERROR, format!("Untitled file {} not found in memory", path)); + return Range::default(); + } //file not in cache, let's load rope on the fly match fs::read_to_string(path) { Ok(content) => { @@ -523,13 +541,22 @@ impl FileMgr { pub fn std_range_to_range(&self, session: &mut SessionInfo, path: &String, range: &std::ops::Range) -> Range { - let file = self.files.get(path); + let file = if Self::is_untitled(path) { + self.untitled_files.get(path) + } else { + self.files.get(path) + }; if let Some(file) = file { if file.borrow().file_info_ast.borrow().text_rope.is_none() { file.borrow_mut().prepare_ast(session); } return file.borrow().std_range_to_range(range); } + // For untitled, never try to read from disk + if Self::is_untitled(path) { + session.log_message(MessageType::ERROR, format!("Untitled file {} not found in memory", path)); + return Range::default(); + } //file not in cache, let's load rope on the fly match fs::read_to_string(path) { Ok(content) => { @@ -544,8 +571,20 @@ impl FileMgr { Range::default() } + /// Returns true if the path/uri is an untitled (in-memory) file. + /// by convention, untitled files start with "untitled:". + pub fn is_untitled(path: &str) -> bool { + path.starts_with("untitled:") + } + pub fn update_file_info(&mut self, session: &mut SessionInfo, uri: &str, content: Option<&Vec>, version: Option, force: bool) -> (bool, Rc>) { - let file_info = self.files.entry(uri.to_string()).or_insert_with(|| { + let is_untitled = Self::is_untitled(uri); + let entry = if is_untitled { + self.untitled_files.entry(uri.to_string()) + } else { + self.files.entry(uri.to_string()) + }; + let file_info = entry.or_insert_with(|| { let mut file_info = FileInfo::new(uri.to_string()); file_info.update_diagnostic_filters(session); Rc::new(RefCell::new(file_info)) @@ -555,7 +594,7 @@ impl FileMgr { let mut updated: bool = false; if (version.is_some() && version.unwrap() != -100) || !file_info.borrow().opened || force { let mut file_info_mut = (*return_info).borrow_mut(); - updated = file_info_mut.update(session, uri, content, version, self.is_in_workspace(uri), force); + updated = file_info_mut.update(session, uri, content, version, self.is_in_workspace(uri), force, is_untitled); drop(file_info_mut); } (updated, return_info) @@ -645,28 +684,35 @@ impl FileMgr { } pub fn pathname2uri(s: &String) -> lsp_types::Uri { - let mut slash = ""; - if cfg!(windows) { - slash = "/"; - } - // If the path starts with \\\\, we want to remove it and also set slash to empty string - // Such that we have file://wsl.localhost/ for example - // For normal paths we do want file:///C:/... - // For some editors like PyCharm they use the legacy windows UNC urls so we have file:////wsl.localhost/ - let (replaced, unc) = if s.starts_with("\\\\") { - slash = ""; - (s.replacen("\\\\", "", 1), true) + let pre_uri = if s.starts_with("untitled:"){ + s.clone() } else { - (s.clone(), false) - }; - // Use legacy UNC flag to determine if we need four slashes - let mut pre_uri = match url::Url::parse(&format!("file://{}{}", slash, replaced)) { - Ok(pre_uri) => pre_uri.to_string(), - Err(err) => panic!("unable to transform pathname to uri: {s}, {}", err) + let mut slash = ""; + if cfg!(windows) { + slash = "/"; + } + // If the path starts with \\\\, we want to remove it and also set slash to empty string + // Such that we have file://wsl.localhost/ for example + // For normal paths we do want file:///C:/... + // For some editors like PyCharm they use the legacy windows UNC urls so we have file:////wsl.localhost/ + let (replaced, unc) = if s.starts_with("\\\\") { + slash = ""; + (s.replacen("\\\\", "", 1), true) + } else { + (s.clone(), false) + }; + // Use legacy UNC flag to determine if we need four slashes + match url::Url::parse(&format!("file://{}{}", slash, replaced)) { + Ok(pre_uri) => { + if unc && legacy_unc_paths().load(Ordering::Relaxed){ + pre_uri.to_string().replace("file://", "file:////") + } else { + pre_uri.to_string() + } + }, + Err(err) => panic!("unable to transform pathname to uri: {s}, {}", err) + } }; - if unc && legacy_unc_paths().load(Ordering::Relaxed){ - pre_uri = pre_uri.replace("file://", "file:////"); - } match lsp_types::Uri::from_str(&pre_uri) { Ok(url) => url, Err(err) => panic!("unable to transform pathname to uri: {s}, {}", err) diff --git a/server/src/core/odoo.rs b/server/src/core/odoo.rs index 52cc21a5..f76e698f 100644 --- a/server/src/core/odoo.rs +++ b/server/src/core/odoo.rs @@ -914,7 +914,7 @@ impl SyncOdoo { self.file_mgr.clone() } - pub fn _unload_path(session: &mut SessionInfo, path: &PathBuf, clean_cache: bool) -> Vec>> { + pub fn unload_path(session: &mut SessionInfo, path: &PathBuf, clean_cache: bool) -> Vec>> { let mut parents = vec![]; let ep_mgr = session.sync_odoo.entry_point_mgr.clone(); for entry in ep_mgr.borrow().iter_all() { @@ -964,7 +964,8 @@ impl SyncOdoo { */ pub fn get_symbol_of_opened_file(session: &mut SessionInfo, path: &PathBuf) -> Option>> { let path_in_tree = path.to_tree_path(); - for entry in session.sync_odoo.entry_point_mgr.borrow().iter_main() { + let ep_mgr = session.sync_odoo.entry_point_mgr.clone(); + for entry in ep_mgr.borrow().iter_main() { let sym_in_data = entry.borrow().data_symbols.get(path.sanitize().as_str()).cloned(); if let Some(sym) = sym_in_data { if let Some(sym) = sym.upgrade() { @@ -983,7 +984,7 @@ impl SyncOdoo { } //Not found? Then return if it is matching a non-public entry strictly matching the file let mut found_an_entry = false; //there to ensure that a wrongly built entry would create infinite loop - for entry in session.sync_odoo.entry_point_mgr.borrow().custom_entry_points.iter() { + for entry in ep_mgr.borrow().custom_entry_points.iter() { let sym_in_data = entry.borrow().data_symbols.get(path.sanitize().as_str()).cloned(); if let Some(sym) = sym_in_data { if let Some(sym) = sym.upgrade() { @@ -1001,6 +1002,15 @@ impl SyncOdoo { return Some(path_symbol[0].clone()); } } + for entry in ep_mgr.borrow().untitled_entry_points.iter() { + if entry.borrow().path == path.sanitize() { + let name = path.with_extension("").components().last().unwrap().as_os_str().to_str().unwrap().to_string(); + let Some(file) = entry.borrow().root.borrow().as_root().module_symbols.get(&Sy!(name)).cloned() else { + continue; + }; + return Some(file); + } + } if !found_an_entry { info!("Path {} not found. Creating new entry", path.to_str().expect("unable to stringify path")); if EntryPointMgr::create_new_custom_entry_for_path(session, &path_in_tree.sanitize(), &path.sanitize()) { @@ -1274,29 +1284,39 @@ impl Odoo { params.text_document_position_params.text_document.uri.to_string(), params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - let uri = params.text_document_position_params.text_document.uri.to_string(); - let path = FileMgr::uri2pathname(uri.as_str()); - if uri.ends_with(".py") || uri.ends_with(".pyi") || uri.ends_with(".xml") || uri.ends_with(".csv") { - if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &PathBuf::from(path.clone())) { - let file_info = session.sync_odoo.get_file_mgr().borrow_mut().get_file_info(&path); - if let Some(file_info) = file_info { - if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { - file_info.borrow_mut().prepare_ast(session); - } - let ast_type = file_info.borrow().file_info_ast.borrow().ast_type.clone(); - match ast_type { - AstType::Python => { - if file_info.borrow_mut().file_info_ast.borrow().indexed_module.is_some() { - return Ok(HoverFeature::hover_python(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - } - }, - AstType::Xml => { - return Ok(HoverFeature::hover_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - }, - AstType::Csv => { - return Ok(HoverFeature::hover_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - }, - } + let path = match params.text_document_position_params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + let uri = params.text_document_position_params.text_document.uri.to_string(); + if !uri.ends_with(".py") && !uri.ends_with(".xml") && !uri.ends_with(".csv") { + return Ok(None); + } + match params.text_document_position_params.text_document.uri.to_file_path(){ + Ok(path) => path.sanitize(), + Err(_) => return Ok(None), + } + }, + Some(schema) if schema == "untitled" => params.text_document_position_params.text_document.uri.to_string(), + _ => return Ok(None), + }; + if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &PathBuf::from(path.clone())) { + let file_info = session.sync_odoo.get_file_mgr().borrow_mut().get_file_info(&path); + if let Some(file_info) = file_info { + if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { + file_info.borrow_mut().prepare_ast(session); + } + let ast_type = file_info.borrow().file_info_ast.borrow().ast_type.clone(); + match ast_type { + AstType::Python => { + if file_info.borrow_mut().file_info_ast.borrow().indexed_module.is_some() { + return Ok(HoverFeature::hover_python(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } + }, + AstType::Xml => { + return Ok(HoverFeature::hover_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + }, + AstType::Csv => { + return Ok(HoverFeature::hover_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + }, } } } @@ -1311,29 +1331,39 @@ impl Odoo { params.text_document_position_params.text_document.uri.to_string(), params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - let uri = params.text_document_position_params.text_document.uri.to_string(); - let path = FileMgr::uri2pathname(uri.as_str()); - if uri.ends_with(".py") || uri.ends_with(".pyi") ||uri.ends_with(".xml") || uri.ends_with(".csv") { - if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &PathBuf::from(path.clone())) { - let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); - if let Some(file_info) = file_info { - if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { - file_info.borrow_mut().prepare_ast(session); - } - let ast_type = file_info.borrow().file_info_ast.borrow().ast_type.clone(); - match ast_type { - AstType::Python => { - if file_info.borrow().file_info_ast.borrow().indexed_module.is_some() { - return Ok(DefinitionFeature::get_location(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - } - }, - AstType::Xml => { - return Ok(DefinitionFeature::get_location_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - }, - AstType::Csv => { - return Ok(DefinitionFeature::get_location_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); - }, - } + let path = match params.text_document_position_params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + let uri = params.text_document_position_params.text_document.uri.to_string(); + if !uri.ends_with(".py") && !uri.ends_with(".xml") && !uri.ends_with(".csv") { + return Ok(None); + } + match params.text_document_position_params.text_document.uri.to_file_path(){ + Ok(path) => path.sanitize(), + Err(_) => return Ok(None), + } + }, + Some(schema) if schema == "untitled" => params.text_document_position_params.text_document.uri.to_string(), + _ => return Ok(None), + }; + if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &PathBuf::from(path.clone())) { + let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); + if let Some(file_info) = file_info { + if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { + file_info.borrow_mut().prepare_ast(session); + } + let ast_type = file_info.borrow().file_info_ast.borrow().ast_type.clone(); + match ast_type { + AstType::Python => { + if file_info.borrow().file_info_ast.borrow().indexed_module.is_some() { + return Ok(DefinitionFeature::get_location(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + } + }, + AstType::Xml => { + return Ok(DefinitionFeature::get_location_xml(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + }, + AstType::Csv => { + return Ok(DefinitionFeature::get_location_csv(session, &file_symbol, &file_info, params.text_document_position_params.position.line, params.text_document_position_params.position.character)); + }, } } } @@ -1386,18 +1416,29 @@ impl Odoo { params.text_document_position.position.line, params.text_document_position.position.character )); - let uri = params.text_document_position.text_document.uri.to_string(); - let path = FileMgr::uri2pathname(uri.as_str()); - if uri.ends_with(".py") ||uri.ends_with(".xml") || uri.ends_with(".csv") { - if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &PathBuf::from(path.clone())) { - let file_info = session.sync_odoo.get_file_mgr().borrow_mut().get_file_info(&path); - if let Some(file_info) = file_info { - if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { - file_info.borrow_mut().prepare_ast(session); - } - if file_info.borrow_mut().file_info_ast.borrow().indexed_module.is_some() { - return Ok(CompletionFeature::autocomplete(session, &file_symbol, &file_info, params.text_document_position.position.line, params.text_document_position.position.character)); - } + let (schema, path) = match params.text_document_position.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + let uri = params.text_document_position.text_document.uri.to_string(); + if !uri.ends_with(".py") && !uri.ends_with(".xml") && !uri.ends_with(".csv") { + return Ok(None); + } + match params.text_document_position.text_document.uri.to_file_path(){ + Ok(path) => (schema, path.sanitize()), + Err(_) => return Ok(None), + } + }, + Some(schema) if schema == "untitled" => (schema, params.text_document_position.text_document.uri.to_string()), + _ => return Ok(None) + }; + let path_buf = PathBuf::from(path.clone()); + if let Some(file_symbol) = SyncOdoo::get_symbol_of_opened_file(session, &path_buf) { + let file_info = session.sync_odoo.get_file_mgr().borrow_mut().get_file_info(&path); + if let Some(file_info) = file_info { + if schema != "untitled" && file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { + file_info.borrow_mut().prepare_ast(session); + } + if file_info.borrow_mut().file_info_ast.borrow().indexed_module.is_some() { + return Ok(CompletionFeature::autocomplete(session, &file_symbol, &file_info, params.text_document_position.position.line, params.text_document_position.position.character)); } } } @@ -1461,9 +1502,10 @@ impl Odoo { continue; //config file update, handled by the config file handler } session.log_message(MessageType::INFO, format!("File update: {}", path.sanitize())); - let (valid, updated) = Odoo::update_file_cache(session, path.clone(), None, -100); + let file_extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + let (valid, updated) = Odoo::update_file_cache(session, &path.sanitize(), file_extension, None, -100); if valid && updated { - Odoo::update_file_index(session, path, false, true); + Odoo::update_file_index(session, path.clone(), file_extension, false, true); } } } @@ -1471,52 +1513,112 @@ impl Odoo { pub fn handle_did_open(session: &mut SessionInfo, params: DidOpenTextDocumentParams) { //to implement Incremental update of file caches, we have to handle DidOpen notification, to be sure // that we use the same base version of the file for future incrementation. - if let Ok(path) = params.text_document.uri.to_file_path() { //temp file has no file path - session.log_message(MessageType::INFO, format!("File opened: {}", path.sanitize())); - let (valid, updated) = Odoo::update_file_cache(session, path.clone(), Some(&vec![TextDocumentContentChangeEvent{ - range: None, - range_length: None, - text: params.text_document.text}]), params.text_document.version); - if valid { - session.sync_odoo.opened_files.push(path.sanitize()); - if session.sync_odoo.state_init == InitState::NOT_READY { - return - } - let tree = session.sync_odoo.path_to_main_entry_tree(&path); - let tree_path = path.to_tree_path(); - if tree.is_none() || - (session.sync_odoo.get_main_entry().borrow().root.borrow().get_symbol(tree.as_ref().unwrap(), u32::MAX).is_empty() - && session.sync_odoo.get_main_entry().borrow().data_symbols.get(&path.sanitize()).is_none()) - { - //main entry doesn't handle this file. Let's test customs entries, or create a new one - let ep_mgr = session.sync_odoo.entry_point_mgr.clone(); - for custom_entry in ep_mgr.borrow().custom_entry_points.iter() { - if custom_entry.borrow().path == tree_path.sanitize() { - if updated{ - Odoo::update_file_index(session, path, true, false); + match params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + match params.text_document.uri.to_file_path(){ + Ok(path) => { + let sanitized_path = path.sanitize(); + session.log_message(MessageType::INFO, format!("File opened: {}", sanitized_path)); + let file_extension = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + let (valid, updated) = Odoo::update_file_cache(session, &sanitized_path, file_extension, Some(&vec![TextDocumentContentChangeEvent{ + range: None, + range_length: None, + text: params.text_document.text + }]), params.text_document.version); + if valid { + session.sync_odoo.opened_files.push(sanitized_path.clone()); + if session.sync_odoo.state_init == InitState::NOT_READY { + return + } + let tree = session.sync_odoo.path_to_main_entry_tree(&path); + let tree_path = path.to_tree_path(); + if tree.is_none() || + (session.sync_odoo.get_main_entry().borrow().root.borrow().get_symbol(tree.as_ref().unwrap(), u32::MAX).is_empty() + && session.sync_odoo.get_main_entry().borrow().data_symbols.get(&path.sanitize()).is_none()) + { + //main entry doesn't handle this file. Let's test customs entries, or create a new one + let ep_mgr = session.sync_odoo.entry_point_mgr.clone(); + for custom_entry in ep_mgr.borrow().custom_entry_points.iter() { + if custom_entry.borrow().path == tree_path.sanitize() { + if updated{ + Odoo::update_file_index(session, path.clone(), file_extension, true, false); + } + return; + } + } + EntryPointMgr::create_new_custom_entry_for_path(session, &tree_path.sanitize(), &sanitized_path); + SyncOdoo::process_rebuilds(session, false); + } else if updated { + Odoo::update_file_index(session, path.clone(), file_extension, true, false); } - return; } + }, + Err(_) => { + warn!("Unable to get file path from URI: {}", params.text_document.uri.to_string()); + return; } - EntryPointMgr::create_new_custom_entry_for_path(session, &tree_path.sanitize(), &path.sanitize()); - SyncOdoo::process_rebuilds(session, false); - } else if updated { - Odoo::update_file_index(session, path, true, false); } + }, + Some(schema) if schema == "untitled" => { + if !["python"].contains(¶ms.text_document.language_id.as_str()) { + return; // We only handle python temporary files + } + let path = params.text_document.uri.to_string(); // In VSCode it is Untitled-N + let (valid, updated) = Odoo::update_file_cache(session, &path, "py", Some(&vec![TextDocumentContentChangeEvent{ + range: None, + range_length: None, + text: params.text_document.text + }]), params.text_document.version); + if valid { + session.sync_odoo.opened_files.push(path.clone()); + if session.sync_odoo.state_init == InitState::NOT_READY { + return + } + if updated { + SyncOdoo::unload_path(session, &PathBuf::from(&path), false); + } + } + EntryPointMgr::create_new_untitled_entry_for_path(session, &path); + SyncOdoo::process_rebuilds(session, false); + }, // temporary file + Some(scheme) => { + warn!("Unsupported URI scheme: {}", scheme); + }, + None => { + warn!("No URI scheme found"); } } } pub fn handle_did_close(session: &mut SessionInfo, params: DidCloseTextDocumentParams) { - if let Ok(path) = params.text_document.uri.to_file_path().map(|path_buf| path_buf.sanitize()) { - session.log_message(MessageType::INFO, format!("File closed: {path}")); - session.sync_odoo.opened_files.retain(|x| x != &path); - let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); - if let Some(file_info) = file_info { - file_info.borrow_mut().opened = false; + let path = match params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + match params.text_document.uri.to_file_path().map(|path_buf| path_buf.sanitize()){ + Ok(path) => path, + Err(_) => { + warn!("Unable to get file path from URI: {}", params.text_document.uri.to_string()); + return; + } + } + }, + Some(schema) if schema == "untitled" =>params.text_document.uri.to_string(), + Some(scheme) => { + warn!("Unsupported URI scheme: {}", scheme); + return; + }, + None => { + warn!("No URI scheme found"); + return; } - session.sync_odoo.entry_point_mgr.borrow_mut().remove_entries_with_path(&path); + }; + session.log_message(MessageType::INFO, format!("File closed: {path}")); + session.sync_odoo.opened_files.retain(|x| x != &path); + let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); + if let Some(file_info) = file_info { + file_info.borrow_mut().opened = false; + file_info.borrow_mut().version = None; } + session.sync_odoo.entry_point_mgr.borrow_mut().remove_entries_with_path(&path); } pub fn search_symbols_to_rebuild(session: &mut SessionInfo, path: &String) { @@ -1576,7 +1678,7 @@ impl Odoo { session.log_message(MessageType::INFO, format!("Renaming {} to {}", old_path, new_path)); //1 - delete old uri session.sync_odoo.opened_files.retain(|x| x != &old_path.clone()); - let _ = SyncOdoo::_unload_path(session, &PathBuf::from(&old_path), false); + let _ = SyncOdoo::unload_path(session, &PathBuf::from(&old_path), false); FileMgr::delete_path(session, &old_path); session.sync_odoo.entry_point_mgr.borrow_mut().remove_entries_with_path(&old_path); SyncOdoo::process_rebuilds(session, false); @@ -1630,7 +1732,7 @@ impl Odoo { let path = FileMgr::uri2pathname(&f.uri); session.log_message(MessageType::INFO, format!("Deleting {}", path)); //1 - delete old uri - let _ = SyncOdoo::_unload_path(session, &PathBuf::from(&path), false); + let _ = SyncOdoo::unload_path(session, &PathBuf::from(&path), false); FileMgr::delete_path(session, &path); session.sync_odoo.entry_point_mgr.borrow_mut().remove_entries_with_path(&path); } @@ -1638,16 +1740,36 @@ impl Odoo { } pub fn handle_did_change(session: &mut SessionInfo, params: DidChangeTextDocumentParams) { - if let Ok(path) = params.text_document.uri.to_file_path() { - session.log_message(MessageType::INFO, format!("File changed: {}", path.sanitize())); - let version = params.text_document.version; - let (valid, updated) = Odoo::update_file_cache(session, path.clone(), Some(¶ms.content_changes), version); - if valid && updated { - if session.sync_odoo.state_init == InitState::NOT_READY { - return + let (scheme, path) = match params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + match params.text_document.uri.to_file_path(){ + Ok(path) => (schema, path.sanitize()), + Err(_) => { + warn!("Unable to get file path from URI: {}", params.text_document.uri.to_string()); + return; + } } - Odoo::update_file_index(session, path, false, false); - } + }, + Some(scheme) if scheme == "untitled" => (scheme, params.text_document.uri.to_string()), + Some(scheme) => { + warn!("Unsupported URI scheme: {}", scheme); + return; + }, + None => { + warn!("No URI scheme found"); + return; + }, + }; + let path_buf = PathBuf::from(&path); + session.log_message(MessageType::INFO, format!("File changed: {}", path)); + let file_extension = match scheme.as_str() { + "file" => path_buf.extension().clone().and_then(|s| s.to_str()).unwrap_or(""), + "untitled" => "py", + _ => return, + }; + let (valid, updated) = Odoo::update_file_cache(session, &path, file_extension, Some(¶ms.content_changes), params.text_document.version); + if session.sync_odoo.state_init != InitState::NOT_READY && valid && updated { + Odoo::update_file_index(session, path_buf.clone(), file_extension, false, false); } } @@ -1664,18 +1786,18 @@ impl Odoo { // return (valid, updated) booleans // if the file has been updated, is valid for an index reload, and contents have been changed - fn update_file_cache(session: &mut SessionInfo, path: PathBuf, content: Option<&Vec>, version: i32) -> (bool, bool) { - if matches!(path.extension().and_then(OsStr::to_str), Some(ext) if ["py", "xml", "csv"].contains(&ext)) || Odoo::is_config_workspace_file(session, &path){ - session.log_message(MessageType::INFO, format!("File Change Event: {}, version {}", path.to_str().unwrap(), version)); - let (file_updated, file_info) = session.sync_odoo.get_file_mgr().borrow_mut().update_file_info(session, &path.sanitize(), content, Some(version), false); + fn update_file_cache(session: &mut SessionInfo, path: &String, extension: &str, content: Option<&Vec>, version: i32) -> (bool, bool) { + if ["py", "xml", "csv"].contains(&extension) || Odoo::is_config_workspace_file(session, &PathBuf::from(path)){ + session.log_message(MessageType::INFO, format!("File Change Event: {}, version {}", path, version)); + let (file_updated, file_info) = session.sync_odoo.get_file_mgr().borrow_mut().update_file_info(session, path, content, Some(version), false); file_info.borrow_mut().publish_diagnostics(session); //To push potential syntax errors or refresh previous one return (!file_info.borrow().opened || version >= 0, file_updated); } (false, false) } - pub fn update_file_index(session: &mut SessionInfo, path: PathBuf, _is_open: bool, force_delay: bool) { - if matches!(path.extension().and_then(OsStr::to_str), Some(ext) if ["py", "xml", "csv"].contains(&ext)) || Odoo::is_config_workspace_file(session, &path){ + pub fn update_file_index(session: &mut SessionInfo, path: PathBuf, extension: &str, _is_open: bool, force_delay: bool) { + if ["py", "xml", "csv"].contains(&extension) || Odoo::is_config_workspace_file(session, &path){ SessionInfo::request_update_file_index(session, &path, force_delay); } } @@ -1684,16 +1806,36 @@ impl Odoo { session.log_message(MessageType::INFO, format!("Document symbol requested for {}", params.text_document.uri.as_str(), )); - let uri = params.text_document.uri.to_string(); - let path = FileMgr::uri2pathname(uri.as_str()); - if uri.ends_with(".py") || uri.ends_with(".pyi") || uri.ends_with(".xml") || uri.ends_with(".csv") { - let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); - if let Some(file_info) = file_info { - if file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { - file_info.borrow_mut().prepare_ast(session); + let (schema, path) = match params.text_document.uri.scheme().map(|scheme| scheme.to_lowercase()) { + Some(schema) if schema == "file" => { + let uri = params.text_document.uri.to_string(); + if !uri.ends_with(".py") && !uri.ends_with(".pyi") && !uri.ends_with(".xml") && !uri.ends_with(".csv") { + return Ok(None); + } + match params.text_document.uri.to_file_path(){ + Ok(path) => (schema, path.sanitize()), + Err(_) => { + warn!("Unable to get file path from URI: {}", params.text_document.uri.to_string()); + return Ok(None); + } } - return Ok(DocumentSymbolFeature::get_symbols(session, &file_info)); + }, + Some(schema) if schema == "untitled" => (schema, params.text_document.uri.to_string()), + Some(scheme) => { + warn!("Unsupported URI scheme: {}", scheme); + return Ok(None); + }, + None => { + warn!("No URI scheme found"); + return Ok(None); + } + }; + let file_info = session.sync_odoo.get_file_mgr().borrow().get_file_info(&path); + if let Some(file_info) = file_info { + if schema != "untitled" && file_info.borrow().file_info_ast.borrow().indexed_module.is_none() { + file_info.borrow_mut().prepare_ast(session); } + return Ok(DocumentSymbolFeature::get_symbols(session, &file_info)); } Ok(None) } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index c5bf4dca..33960829 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -1296,7 +1296,7 @@ impl Symbol { path.with_extension("").components().last().unwrap().as_os_str().to_str().unwrap().to_string() }; let path_str = path.sanitize(); - if path_str.ends_with(".py") || path_str.ends_with(".pyi") { + if path_str.ends_with(".py") || path_str.ends_with(".pyi") || path_str.starts_with("untitled:") { return Some(parent.borrow_mut().add_new_file(session, &name, &path_str)); } if parent.borrow().get_main_entry_tree(session) == tree(vec!["odoo", "addons"], vec![]) && path.join("__manifest__.py").exists() { diff --git a/server/src/threads.rs b/server/src/threads.rs index 009de199..7be02b1c 100644 --- a/server/src/threads.rs +++ b/server/src/threads.rs @@ -104,7 +104,7 @@ impl <'a> SessionInfo<'a> { let _ = session.delayed_process_sender.as_ref().unwrap().send(DelayedProcessingMessage::RESTART); return; } - let _ = SyncOdoo::_unload_path(session, &path, false); + let _ = SyncOdoo::unload_path(session, &path, false); Odoo::search_symbols_to_rebuild(session, &path.sanitize()); if (!forced_delay || session.delayed_process_sender.is_none()) && !session.sync_odoo.need_rebuild { if session.sync_odoo.get_rebuild_queue_size() < 10 {