diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e991ef..84bfb16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -162,3 +162,19 @@ jobs: prerelease: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish Extension (Code Marketplace, release) + if: env.SHOULD_RELEASE == 'yes' + shell: bash + run: | + cd "vscode" + npm install --include=dev + npx vsce publish --pat ${{ secrets.MARKETPLACE_TOKEN }} --packagePath ../dist/magento2-ls.*.vsix + + - name: Publish Extension (OpenVSX, release) + if: env.SHOULD_RELEASE == 'yes' + shell: bash + run: | + cd "vscode" + npm install --include=dev + npx ovsx publish --pat ${{ secrets.OPENVSX_TOKEN }} --packagePath ../dist/magento2-ls.*.vsix diff --git a/CHANGELOG.md b/CHANGELOG.md index 7774704..c041fd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [0.0.6 - Work In Progress] + +#### Features + +- Added mixins to the definition locations of JS components. +- Added state updates when changes occur in `registration.php` and `requirejs-config.js`. +- Increased indexing speed by searching only specific locations. +- Added suggestions for library modules, such as `magento-framework`. + + ## [0.0.5 - October 4, 2023] #### Features diff --git a/README.md b/README.md index 7d232b0..71d5c5a 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,18 @@ Be PHP Language Server (or XML LS) in any capacity. [Intelephense](https://intelephense.com/) works nice with Magento 2 if you need LS for your project. +## Requirements + +In order to complete Magento classes, the Magento root folder must be opened in the workspace. + +The language server detects Magento modules by searching for `registration.php` files in the following locations: + +- The root folder (for modules added to the workspace) +- `app/code/*/*/` - for local modules +- `vendor/*/*/` - for vendor modules +- `app/design/*/*/*/` - for themes. + + ## Contributing If you would like to contribute, please feel free to submit pull requests or open issues on the [GitHub repository](https://github.com/pbogut/magento2-ls). diff --git a/src/indexer.rs b/src/indexer.rs deleted file mode 100644 index 7e9774b..0000000 --- a/src/indexer.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::Arc, - thread::{spawn, JoinHandle}, - time::SystemTime, -}; - -use lsp_types::Position; -use parking_lot::Mutex; - -use crate::{ - js, - m2::{M2Area, M2Item}, - php, xml, -}; - -trait HashMapId { - fn id(&self) -> usize; -} - -impl HashMapId for M2Area { - fn id(&self) -> usize { - match self { - Self::Frontend => 0, - Self::Adminhtml => 1, - Self::Base => 2, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Indexer { - buffers: HashMap, - magento_modules: Vec, - magento_module_paths: HashMap, - magento_front_themes: HashMap, - magento_admin_themes: HashMap, - js_maps: [HashMap; 3], - js_mixins: HashMap, - workspaces: Vec, -} - -#[allow(clippy::module_name_repetitions)] -pub type ArcIndexer = Arc>; - -impl Indexer { - pub fn new() -> Self { - Self { - buffers: HashMap::new(), - magento_modules: vec![], - magento_module_paths: HashMap::new(), - magento_front_themes: HashMap::new(), - magento_admin_themes: HashMap::new(), - js_maps: [HashMap::new(), HashMap::new(), HashMap::new()], - js_mixins: HashMap::new(), - workspaces: vec![], - } - } - - pub fn set_file(&mut self, path: &Path, content: S) - where - S: Into, - { - self.buffers.insert(path.to_owned(), content.into()); - } - - pub fn get_file(&self, path: &PathBuf) -> Option<&String> { - self.buffers.get(path) - } - - pub fn del_file(&mut self, path: &PathBuf) { - self.buffers.remove(path); - } - - pub fn get_modules(&self) -> Vec { - let mut modules = self.magento_modules.clone(); - modules.sort_unstable(); - modules.dedup(); - modules - } - - pub fn get_module_class_prefixes(&self) -> Vec { - self.get_modules() - .iter() - .map(|m| m.replace('_', "\\")) - .collect() - } - - pub fn get_module_path(&self, module: &str) -> Option { - self.magento_module_paths.get(module).cloned() - } - - pub fn add_module(&mut self, module: &str) -> &mut Self { - self.magento_modules.push(module.to_string()); - self - } - - pub fn add_module_path(&mut self, module: &str, path: PathBuf) -> &mut Self { - self.magento_module_paths.insert(module.to_string(), path); - self - } - - pub fn add_admin_theme_path(&mut self, name: &str, path: PathBuf) { - self.magento_admin_themes.insert(name.to_string(), path); - } - - pub fn add_front_theme_path(&mut self, name: &str, path: PathBuf) { - self.magento_front_themes.insert(name.to_string(), path); - } - - pub fn get_component_map(&self, name: &str, area: &M2Area) -> Option<&String> { - self.js_maps[area.id()].get(name) - } - - pub fn get_component_maps_for_area(&self, area: &M2Area) -> Vec { - self.js_maps[area.id()] - .keys() - .map(ToString::to_string) - .collect() - } - - pub fn add_component_map(&mut self, name: &str, val: S, area: &M2Area) -> Option - where - S: Into, - { - self.js_maps[area.id()].insert(name.to_string(), val.into()) - } - - pub fn add_component_mixin(&mut self, name: &str, val: S) -> Option - where - S: Into, - { - self.js_mixins.insert(name.to_string(), val.into()) - } - - pub fn list_front_themes_paths(&self) -> Vec<&PathBuf> { - self.magento_front_themes - .values() - .collect::>() - } - - pub fn list_admin_themes_paths(&self) -> Vec<&PathBuf> { - self.magento_admin_themes - .values() - .collect::>() - } - - pub fn workspace_paths(&self) -> Vec { - self.workspaces.clone() - } - - pub fn add_workspace_path(&mut self, path: &Path) { - self.workspaces.push(path.to_path_buf()); - } - - pub fn has_workspace_path(&mut self, path: &Path) -> bool { - self.workspaces.contains(&path.to_path_buf()) - } - - pub fn get_item_from_position(&self, path: &PathBuf, pos: Position) -> Option { - match path.extension()?.to_str()?.to_lowercase().as_str() { - "js" => js::get_item_from_position(self, path, pos), - "xml" => xml::get_item_from_position(self, path, pos), - _ => None, - } - } - - pub fn into_arc(self) -> ArcIndexer { - Arc::new(Mutex::new(self)) - } - - pub fn update_index(arc_index: &ArcIndexer, path: &Path) -> Vec> { - let mut index = arc_index.lock(); - if index.has_workspace_path(path) { - vec![] - } else { - index.add_workspace_path(path); - vec![ - spawn_index(arc_index, path, php::update_index, "PHP Indexing"), - spawn_index(arc_index, path, js::update_index, "JS Indexing"), - ] - } - } - - pub fn split_class_to_path_and_suffix(&self, class: &str) -> Option<(PathBuf, Vec)> { - let mut parts = class.split('\\').collect::>(); - let mut suffix = vec![]; - - while let Some(part) = parts.pop() { - suffix.push(part.to_string()); - let prefix = parts.join("\\"); - let module_path = self.get_module_path(&prefix); - match module_path { - Some(mod_path) => { - suffix.reverse(); - return Some((mod_path, suffix)); - } - None => continue, - } - } - None - } -} - -fn spawn_index( - arc_indexer: &ArcIndexer, - path: &Path, - callback: fn(&ArcIndexer, &PathBuf), - msg: &str, -) -> JoinHandle<()> { - let index = Arc::clone(arc_indexer); - let path = path.to_path_buf(); - let msg = msg.to_owned(); - - spawn(move || { - eprintln!("Start {}", msg); - let index_start = SystemTime::now(); - callback(&index, &path); - index_start.elapsed().map_or_else( - |_| eprintln!("{} done", msg), - |d| eprintln!("{} done in {:?}", msg, d), - ); - }) -} diff --git a/src/js.rs b/src/js.rs index 7571d96..13a5a55 100644 --- a/src/js.rs +++ b/src/js.rs @@ -5,9 +5,9 @@ use lsp_types::{Position, Range}; use tree_sitter::{Node, QueryCursor}; use crate::{ - indexer::{ArcIndexer, Indexer}, m2::{M2Area, M2Item, M2Path}, queries, + state::{ArcState, State}, ts::{self, node_at_position}, }; @@ -31,20 +31,48 @@ pub struct JsCompletion { pub kind: JsCompletionType, } -pub fn update_index(index: &ArcIndexer, path: &PathBuf) { - let modules = glob(&path.append(&["**", "requirejs-config.js"]).to_path_string()) - .expect("Failed to read glob pattern"); +pub fn update_index(state: &ArcState, path: &PathBuf) { + // if current workspace is magento module + process_glob(state, &path.append(&["view", "*", "requirejs-config.js"])); + // if current workspace is magento installation + process_glob( + state, + &path.append(&["vendor", "*", "*", "view", "*", "requirejs-config.js"]), + ); + process_glob( + state, + &path.append(&["vendor", "*", "*", "Magento_Theme", "requirejs-config.js"]), + ); + process_glob( + state, + &path.append(&["app", "code", "*", "*", "view", "*", "requirejs-config.js"]), + ); + process_glob( + state, + &path.append(&["app", "design", "**", "requirejs-config.js"]), + ); +} - for require_config in modules { - require_config.map_or_else( - |_e| panic!("buhu"), - |file_path| { - let content = std::fs::read_to_string(&file_path) - .expect("Should have been able to read the file"); +pub fn maybe_index_file(state: &mut State, content: &str, file_path: &PathBuf) { + if file_path.to_path_str().ends_with("requirejs-config.js") { + update_index_from_config(state, content, file_path); + } +} - update_index_from_config(index, &content, &file_path.get_area()); - }, - ); +fn index_file(state: &ArcState, file_path: &PathBuf) { + let content = + std::fs::read_to_string(file_path).expect("Should have been able to read the file"); + + update_index_from_config(&mut state.lock(), &content, file_path); +} + +fn process_glob(state: &ArcState, glob_path: &PathBuf) { + let modules = glob(glob_path.to_path_str()) + .expect("Failed to read glob pattern") + .filter_map(Result::ok); + + for file_path in modules { + index_file(state, &file_path); } } @@ -81,12 +109,12 @@ pub fn get_completion_item(content: &str, pos: Position) -> Option None } -pub fn get_item_from_position(index: &Indexer, path: &PathBuf, pos: Position) -> Option { - let content = index.get_file(path)?; - get_item_from_pos(index, content, path, pos) +pub fn get_item_from_position(state: &State, path: &PathBuf, pos: Position) -> Option { + let content = state.get_file(path)?; + get_item_from_pos(state, content, path, pos) } -fn get_item_from_pos(index: &Indexer, content: &str, path: &Path, pos: Position) -> Option { +fn get_item_from_pos(state: &State, content: &str, path: &Path, pos: Position) -> Option { let tree = tree_sitter_parsers::parse(content, "javascript"); let query = queries::js_item_from_pos(); let mut cursor = QueryCursor::new(); @@ -95,51 +123,55 @@ fn get_item_from_pos(index: &Indexer, content: &str, path: &Path, pos: Position) for m in matches { if node_at_position(m.captures[1].node, pos) { let text = get_node_text(m.captures[1].node, content); - let text = resolve_component_text(index, &text, &path.to_path_buf().get_area())?; - return text_to_component(index, text, path); + let text = resolve_component_text(state, text, &path.to_path_buf().get_area())?; + return text_to_component(state, text, path); } } None } -pub fn resolve_component_text(index: &Indexer, text: &str, area: &M2Area) -> Option { - index.get_component_map(text, area).map_or_else( +pub fn resolve_component_text<'a>( + state: &'a State, + text: &'a str, + area: &M2Area, +) -> Option<&'a str> { + state.get_component_map(text, area).map_or_else( || { - area.lower_area().map_or_else( - || Some(text.to_string()), - |a| resolve_component_text(index, text, &a), - ) + area.lower_area() + .map_or_else(|| Some(text), |a| resolve_component_text(state, text, &a)) }, - |t| resolve_component_text(index, t, area), + |t| resolve_component_text(state, t, area), ) } -pub fn text_to_component(index: &Indexer, text: String, path: &Path) -> Option { +pub fn text_to_component(state: &State, text: &str, path: &Path) -> Option { let begining = text.split('/').next().unwrap_or(""); if begining.chars().next().unwrap_or('a') == '.' { let mut path = path.to_path_buf(); path.pop(); - Some(M2Item::RelComponent(text, path)) + Some(M2Item::RelComponent(text.into(), path)) } else if text.split('/').count() > 1 && begining.matches('_').count() == 1 && begining.chars().next().unwrap_or('a').is_uppercase() { let mut parts = text.splitn(2, '/'); let mod_name = parts.next()?.to_string(); - let mod_path = index.get_module_path(&mod_name)?; + let mod_path = state.get_module_path(&mod_name)?; Some(M2Item::ModComponent( mod_name, - parts.next()?.to_string(), + parts.next()?.into(), mod_path, )) } else { - Some(M2Item::Component(text)) + Some(M2Item::Component(text.into())) } } -fn update_index_from_config(index: &ArcIndexer, content: &str, area: &M2Area) { +fn update_index_from_config(state: &mut State, content: &str, file_path: &PathBuf) { + state.set_source_file(file_path); + let area = &file_path.get_area(); let tree = tree_sitter_parsers::parse(content, "javascript"); let query = queries::js_require_config(); @@ -149,19 +181,16 @@ fn update_index_from_config(index: &ArcIndexer, content: &str, area: &M2Area) { for m in matches { let key = get_node_text(m.captures[2].node, content); let val = get_node_text(m.captures[3].node, content); - { - let mut index = index.lock(); - match get_kind(m.captures[1].node, content) { - Some(JSTypes::Map | JSTypes::Paths) => index.add_component_map(&key, val, area), - Some(JSTypes::Mixins) => index.add_component_mixin(&key, val), - None => continue, - }; - } + match get_kind(m.captures[1].node, content) { + Some(JSTypes::Map | JSTypes::Paths) => state.add_component_map(key, val, area), + Some(JSTypes::Mixins) => state.add_component_mixin(key, val, area), + None => continue, + }; } } fn get_kind(node: Node, content: &str) -> Option { - match get_node_text(node, content).as_str() { + match get_node_text(node, content) { "map" => Some(JSTypes::Map), "paths" => Some(JSTypes::Paths), "mixins" => Some(JSTypes::Mixins), @@ -169,21 +198,17 @@ fn get_kind(node: Node, content: &str) -> Option { } } -fn get_node_text(node: Node, content: &str) -> String { +fn get_node_text<'a>(node: Node, content: &'a str) -> &'a str { let result = node .utf8_text(content.as_bytes()) .unwrap_or("") - .trim_matches('\\') - .to_string(); + .trim_matches('\\'); if node.kind() == "string" { - match get_node_text(node.child(0).unwrap_or(node), content) + get_node_text(node.child(0).unwrap_or(node), content) .chars() .next() - { - Some(trim) => result.trim_matches(trim).to_string(), - None => result, - } + .map_or(result, |trim| result.trim_matches(trim)) } else { result } @@ -197,7 +222,7 @@ mod test { #[test] fn test_update_index_from_config() { - let index = Indexer::new(); + let state = State::new(); let content = r#" var config = { map: { @@ -223,10 +248,10 @@ mod test { }; "#; - let arc_index = index.into_arc(); - update_index_from_config(&arc_index, content, &M2Area::Base); + let arc_state = state.into_arc(); + update_index_from_config(&mut arc_state.lock(), content, &PathBuf::from("")); - let mut result = Indexer::new(); + let mut result = State::new(); result.add_component_map( "other/core/extension", "Other_Module/js/core_ext", @@ -243,11 +268,33 @@ mod test { &M2Area::Base, ); result.add_component_map("otherComp", "Some_Other/js/comp", &M2Area::Base); - result.add_component_mixin("Mage_Module/js/smth", "My_Module/js/mixin/smth"); - result.add_component_mixin("Adobe_Module", "My_Module/js/mixin/adobe"); + result.add_component_mixin( + "Mage_Module/js/smth", + "My_Module/js/mixin/smth", + &M2Area::Base, + ); + result.add_component_mixin("Adobe_Module", "My_Module/js/mixin/adobe", &M2Area::Base); + result.set_source_file(&PathBuf::from("")); - // FIX fix test without using to_owned - // assert_eq!(arc_index.lock().to_owned(), result); + let computed = arc_state.lock(); + assert_eq!(computed.get_modules(), result.get_modules()); + for module in [ + "prototype", + "otherComp", + "other/core/extension", + "some/js/component", + ] { + assert_eq!( + computed.get_component_map(module, &M2Area::Base), + result.get_component_map(module, &M2Area::Base) + ); + } + for mixin in ["Mage_Module/js/smth", "Adobe_Module"] { + assert_eq!( + computed.get_component_mixins_for_area(mixin, &M2Area::Base), + result.get_component_mixins_for_area(mixin, &M2Area::Base) + ); + } } #[test] @@ -263,8 +310,8 @@ mod test { assert_eq!( item, Some(M2Item::ModComponent( - "Some_Module".to_string(), - "some/view".to_string(), + "Some_Module".into(), + "some/view".into(), PathBuf::from("/a/b/c/Some_Module") )) ); @@ -280,7 +327,7 @@ mod test { "#, "/a/b/c", ); - assert_eq!(item, Some(M2Item::Component("jquery".to_string()))); + assert_eq!(item, Some(M2Item::Component("jquery".into()))); } #[test] @@ -295,7 +342,7 @@ mod test { ); assert_eq!( item, - Some(M2Item::Component("jquery-ui-modules/widget".to_string())) + Some(M2Item::Component("jquery-ui-modules/widget".into())) ); } @@ -312,8 +359,8 @@ mod test { } let pos = Position { line, character }; let uri = PathBuf::from(if cfg!(windows) { &win_path } else { path }); - let mut index = Indexer::new(); - index.add_module_path("Some_Module", PathBuf::from("/a/b/c/Some_Module")); - get_item_from_pos(&index, &xml.replace('|', ""), &uri, pos) + let mut state = State::new(); + state.add_module_path("Some_Module", PathBuf::from("/a/b/c/Some_Module")); + get_item_from_pos(&state, &xml.replace('|', ""), &uri, pos) } } diff --git a/src/lsp.rs b/src/lsp.rs index 9b63d34..8e7724e 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -5,20 +5,18 @@ use lsp_types::{ CompletionParams, CompletionResponse, GotoDefinitionParams, GotoDefinitionResponse, }; -use crate::indexer::ArcIndexer; +use crate::state::State; use self::{completion::get_completion_from_params, definition::get_location_from_params}; -pub fn completion_handler(indexer: &ArcIndexer, params: &CompletionParams) -> CompletionResponse { + +pub fn completion_handler(state: &State, params: &CompletionParams) -> CompletionResponse { CompletionResponse::Array( - get_completion_from_params(indexer, params).map_or(vec![], |loc_list| loc_list), + get_completion_from_params(state, params).map_or(vec![], |loc_list| loc_list), ) } -pub fn definition_handler( - indexer: &ArcIndexer, - params: &GotoDefinitionParams, -) -> GotoDefinitionResponse { +pub fn definition_handler(state: &State, params: &GotoDefinitionParams) -> GotoDefinitionResponse { GotoDefinitionResponse::Array( - get_location_from_params(indexer, params).map_or(vec![], |loc_list| loc_list), + get_location_from_params(state, params).map_or(vec![], |loc_list| loc_list), ) } diff --git a/src/lsp/completion.rs b/src/lsp/completion.rs index 6f364d5..433905b 100644 --- a/src/lsp/completion.rs +++ b/src/lsp/completion.rs @@ -9,14 +9,14 @@ use lsp_types::{ }; use crate::{ - indexer::ArcIndexer, js::{self, JsCompletionType}, m2::{self, M2Area, M2Path, M2Uri}, + state::State, xml, }; pub fn get_completion_from_params( - index: &ArcIndexer, + state: &State, params: &CompletionParams, ) -> Option> { let path = params @@ -25,26 +25,24 @@ pub fn get_completion_from_params( .uri .to_path_buf(); let pos = params.text_document_position.position; - let content = index.lock().get_file(&path)?.clone(); match path.get_ext().as_str() { - "xml" => xml_completion_handler(index, &content, &path, pos), - "js" => js_completion_handler(index, &content, &path, pos), + "xml" => xml_completion_handler(state, &path, pos), + "js" => js_completion_handler(state, &path, pos), _ => None, } } fn js_completion_handler( - index: &ArcIndexer, - content: &str, + state: &State, path: &PathBuf, pos: Position, ) -> Option> { - let at_position = js::get_completion_item(content, pos)?; + let at_position = js::get_completion_item(state.get_file(path)?, pos)?; match at_position.kind { JsCompletionType::Definition => completion_for_component( - index, + state, &at_position.text, at_position.range, &path.get_area(), @@ -53,147 +51,140 @@ fn js_completion_handler( } fn xml_completion_handler( - index: &ArcIndexer, - content: &str, + state: &State, path: &PathBuf, pos: Position, ) -> Option> { - let at_position = xml::get_current_position_path(content, pos)?; + let at_position = xml::get_current_position_path(state.get_file(path)?, pos)?; match at_position { x if x.match_path("[@template]") => { - completion_for_template(index, &x.text, x.range, &path.get_area()) + completion_for_template(state, &x.text, x.range, &path.get_area()) } x if x.attribute_eq("xsi:type", "string") && x.attribute_eq("name", "template") => { - completion_for_template(index, &x.text, x.range, &path.get_area()) + completion_for_template(state, &x.text, x.range, &path.get_area()) } x if x.attribute_eq("xsi:type", "string") && x.attribute_eq("name", "component") => { - completion_for_component(index, &x.text, x.range, &path.get_area()) + completion_for_component(state, &x.text, x.range, &path.get_area()) } x if x.match_path("/config/event[@name]") && path.ends_with("events.xml") => { Some(events::get_completion_items(x.range)) } x if x.match_path("/config/preference[@for]") && path.ends_with("di.xml") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } x if x.match_path("/config/preference[@type]") && path.ends_with("di.xml") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } x if x.match_path("/virtualType[@type]") && path.ends_with("di.xml") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } x if x.match_path("[@class]") || x.match_path("[@instance]") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } x if x.attribute_in("xsi:type", &["object", "const", "init_parameter"]) => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } - x if x.match_path("/type[@name]") => completion_for_classes(index, &x.text, x.range), + x if x.match_path("/type[@name]") => completion_for_classes(state, &x.text, x.range), // Should be /source_model[$text], but html parser dont like undersores x if x.match_path("/source[$text]") && x.attribute_eq("_model", "") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } // Should be /backend_model[$text], but html parser dont like undersores x if x.match_path("/backend[$text]") && x.attribute_eq("_model", "") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } // Should be /frontend_model[$text], but html parser dont like undersores x if x.match_path("/frontend[$text]") && x.attribute_eq("_model", "") => { - completion_for_classes(index, &x.text, x.range) + completion_for_classes(state, &x.text, x.range) } _ => None, } } -fn completion_for_classes( - index: &ArcIndexer, - text: &str, - range: Range, -) -> Option> { +fn completion_for_classes(state: &State, text: &str, range: Range) -> Option> { let text = text.trim_start_matches('\\'); if text.is_empty() || (m2::is_part_of_class_name(text) && text.matches('\\').count() == 0) { - Some(completion_for_classes_prefix(index, range)) - } else if text.matches('\\').count() == 1 { - let mut result = completion_for_classes_prefix(index, range); - if let Some(classes) = completion_for_classes_full(index, text, range) { - result.extend(classes); - } + Some(completion_for_classes_prefix(state, range)) + } else if text.matches('\\').count() >= 1 { + let mut result = completion_for_classes_prefix(state, range); + result.extend(completion_for_classes_full(state, text, range)); Some(result) - } else if text.matches('\\').count() >= 2 { - completion_for_classes_full(index, text, range) } else { None } } -fn completion_for_classes_prefix(index: &ArcIndexer, range: Range) -> Vec { - let module_prefixes = index.lock().get_module_class_prefixes(); +fn completion_for_classes_prefix(state: &State, range: Range) -> Vec { + let module_prefixes = state.get_module_class_prefixes(); string_vec_and_range_to_completion_list(module_prefixes, range) } -fn completion_for_classes_full( - index: &ArcIndexer, - text: &str, - range: Range, -) -> Option> { - let mut parts = text.split('\\'); - - let module_name = format!("{}_{}", parts.next()?, parts.next()?); +fn completion_for_classes_full(state: &State, text: &str, range: Range) -> Vec { + let mut classes = vec![]; + let mut index = 0; + let splits: Vec = text + .chars() + .filter_map(|c| { + index += 1; + if c == '\\' { + Some(index) + } else { + None + } + }) + .collect(); - let mut parts = text.split('\\').collect::>(); - parts.pop(); - let typed_class_prefix = parts.join("\\"); + for spllit in splits { + let prefix = &text[..spllit - 1]; + if let Some(module_path) = state.get_module_path(prefix) { + let candidates = glob(module_path.append(&["**", "*.php"]).to_path_str()) + .expect("Failed to read glob pattern"); + for p in candidates { + let path = p.map_or_else(|_| std::path::PathBuf::new(), |p| p); + let rel_path = path.relative_to(&module_path).str_components().join("\\"); + let class_suffix = rel_path.trim_end_matches(".php"); + let class = format!("{}\\{}", prefix, class_suffix); - let module_class = module_name.replace('_', "\\"); - let module_path = index.lock().get_module_path(&module_name)?; - let candidates = glob(&module_path.append(&["**", "*.php"]).to_path_string()) - .expect("Failed to read glob pattern"); - let mut classes = vec![]; - for p in candidates { - let path = p.map_or_else(|_| std::path::PathBuf::new(), |p| p); - let rel_path = path - .relative_to(&module_path) - .string_components() - .join("\\"); - let class_suffix = rel_path.trim_end_matches(".php"); - let class = format!("{}\\{}", &module_class, class_suffix); + if class.ends_with("\\registration") { + continue; + } - if class.ends_with("\\registration") { - continue; - } + if !class.starts_with(&text[..index - 1]) { + continue; + } - if !class.starts_with(&typed_class_prefix) { - continue; + classes.push(class); + } } - - classes.push(class); } - Some(string_vec_and_range_to_completion_list(classes, range)) + + string_vec_and_range_to_completion_list(classes, range) } fn completion_for_template( - index: &ArcIndexer, + state: &State, text: &str, range: Range, area: &M2Area, ) -> Option> { if text.is_empty() || m2::is_part_of_module_name(text) { - let modules = index.lock().get_modules(); + let modules = state.get_modules(); Some(string_vec_and_range_to_completion_list(modules, range)) } else if text.contains("::") { let module_name = text.split("::").next()?; - let path = index.lock().get_module_path(module_name); + let path = state.get_module_path(module_name); match path { None => None, Some(path) => { let mut files = vec![]; for area_string in area.path_candidates() { - let view_path = path.append(&["view", &area_string, "templates"]); + let view_path = path.append(&["view", area_string, "templates"]); let glob_path = view_path.append(&["**", "*.phtml"]); - files.extend(glob::glob(&glob_path.to_path_string()).ok()?.map(|file| { + files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { let path = file .unwrap_or_default() .relative_to(&view_path) - .string_components() + .str_components() .join("/"); String::from(module_name) + "::" + &path })); @@ -207,7 +198,7 @@ fn completion_for_template( } fn completion_for_component( - index: &ArcIndexer, + state: &State, text: &str, range: Range, area: &M2Area, @@ -215,56 +206,56 @@ fn completion_for_component( if text.contains('/') { let module_name = text.split('/').next()?; let mut files = vec![]; - if let Some(path) = index.lock().get_module_path(module_name) { + if let Some(path) = state.get_module_path(module_name) { for area in area.path_candidates() { - let view_path = path.append(&["view", &area, "web"]); + let view_path = path.append(&["view", area, "web"]); let glob_path = view_path.append(&["**", "*.js"]); - files.extend(glob::glob(&glob_path.to_path_string()).ok()?.map(|file| { + files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { let path = file .unwrap_or_default() .relative_to(&view_path) - .string_components() + .str_components() .join("/"); let path = path.trim_end_matches(".js"); String::from(module_name) + "/" + path })); } } - let workspaces = index.lock().workspace_paths(); + let workspaces = state.workspace_paths(); for path in workspaces { let view_path = path.append(&["lib", "web"]); let glob_path = view_path.append(&["**", "*.js"]); - files.extend(glob::glob(&glob_path.to_path_string()).ok()?.map(|file| { + files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { let path = file .unwrap_or_default() .relative_to(&view_path) - .string_components() + .str_components() .join("/"); path.trim_end_matches(".js").to_string() })); } - files.extend(index.lock().get_component_maps_for_area(area)); + files.extend(state.get_component_maps_for_area(area)); if let Some(lower_area) = area.lower_area() { - files.extend(index.lock().get_component_maps_for_area(&lower_area)); + files.extend(state.get_component_maps_for_area(&lower_area)); } Some(string_vec_and_range_to_completion_list(files, range)) } else { let mut modules = vec![]; - modules.extend(index.lock().get_modules()); - modules.extend(index.lock().get_component_maps_for_area(area)); + modules.extend(state.get_modules()); + modules.extend(state.get_component_maps_for_area(area)); if let Some(lower_area) = area.lower_area() { - modules.extend(index.lock().get_component_maps_for_area(&lower_area)); + modules.extend(state.get_component_maps_for_area(&lower_area)); } - let workspaces = index.lock().workspace_paths(); + let workspaces = state.workspace_paths(); for path in workspaces { let view_path = path.append(&["lib", "web"]); let glob_path = view_path.append(&["**", "*.js"]); - modules.extend(glob::glob(&glob_path.to_path_string()).ok()?.map(|file| { + modules.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { let path = file .unwrap_or_default() .relative_to(&view_path) - .string_components() + .str_components() .join("/"); path.trim_end_matches(".js").to_string() })); diff --git a/src/lsp/definition.rs b/src/lsp/definition.rs index 71bded0..6f0cf9d 100644 --- a/src/lsp/definition.rs +++ b/src/lsp/definition.rs @@ -1,16 +1,18 @@ +mod component; +mod php; +mod phtml; + use std::path::Path; use lsp_types::{GotoDefinitionParams, Location, Range, Url}; -use parking_lot::MutexGuard; use crate::{ - indexer::{ArcIndexer, Indexer}, - m2::{M2Area, M2Item, M2Path, M2Uri}, - php::{self, PHPClass}, + m2::{M2Item, M2Uri}, + state::State, }; pub fn get_location_from_params( - index: &ArcIndexer, + state: &State, params: &GotoDefinitionParams, ) -> Option> { let path = params @@ -19,153 +21,20 @@ pub fn get_location_from_params( .uri .to_path_buf(); let pos = params.text_document_position_params.position; - - let item = index.lock().get_item_from_position(&path, pos); - match item { - Some(M2Item::ModComponent(_mod_name, file_path, mod_path)) => { - let mut result = vec![]; - - for area in [M2Area::Frontend, M2Area::Adminhtml, M2Area::Base] { - let comp_path = mod_path - .append(&["view", &area.to_string(), "web", &file_path]) - .append_ext("js"); - if let Some(location) = path_to_location(&comp_path) { - result.push(location); - } - } - - Some(result) - } - Some(M2Item::RelComponent(comp, path)) => { - let mut path = path.join(comp); - path.set_extension("js"); - path_to_location(&path).map(|location| vec![location]) - } - Some(M2Item::Component(comp)) => { - let mut result = vec![]; - let workspace_paths = index.lock().workspace_paths(); - for path in workspace_paths { - let path = path.append(&["lib", "web", &comp]).append_ext("js"); - if let Some(location) = path_to_location(&path) { - result.push(location); - } - } - Some(result) - } - Some(M2Item::AdminPhtml(mod_name, template)) => { - let mut result = vec![]; - add_phtml_in_mod_location(index, &mut result, &mod_name, &template, &M2Area::Adminhtml); - add_phtml_in_admin_theme_location(index, &mut result, &mod_name, &template); - Some(result) - } - Some(M2Item::FrontPhtml(mod_name, template)) => { - let mut result = vec![]; - add_phtml_in_mod_location(index, &mut result, &mod_name, &template, &M2Area::Frontend); - add_phtml_in_front_theme_location(index, &mut result, &mod_name, &template); - Some(result) - } - Some(M2Item::BasePhtml(mod_name, template)) => { - let mut result = vec![]; - add_phtml_in_mod_location(index, &mut result, &mod_name, &template, &M2Area::Base); - add_phtml_in_front_theme_location(index, &mut result, &mod_name, &template); - add_phtml_in_admin_theme_location(index, &mut result, &mod_name, &template); - Some(result) - } - Some(M2Item::Class(class)) => { - let phpclass = get_php_class_from_class_name(&index.lock(), &class)?; - Some(vec![Location { - uri: phpclass.uri.clone(), - range: phpclass.range, - }]) - } - Some(M2Item::Method(class, method)) => { - let phpclass = get_php_class_from_class_name(&index.lock(), &class)?; - Some(vec![Location { - uri: phpclass.uri.clone(), - range: phpclass - .methods - .get(&method) - .map_or(phpclass.range, |method| method.range), - }]) - } - Some(M2Item::Const(class, constant)) => { - let phpclass = get_php_class_from_class_name(&index.lock(), &class)?; - Some(vec![Location { - uri: phpclass.uri.clone(), - range: phpclass - .constants - .get(&constant) - .map_or(phpclass.range, |method| method.range), - }]) - } - None => None, - } -} - -fn add_phtml_in_mod_location( - index: &ArcIndexer, - result: &mut Vec, - mod_name: &str, - template: &str, - area: &M2Area, -) { - let mod_path = index.lock().get_module_path(mod_name); - if let Some(path) = mod_path { - for area in area.path_candidates() { - let templ_path = path.append(&["view", &area, "templates", template]); - if let Some(location) = path_to_location(&templ_path) { - result.push(location); - } - } - } -} - -fn add_phtml_in_admin_theme_location( - index: &ArcIndexer, - result: &mut Vec, - mod_name: &str, - template: &str, -) { - #[allow(clippy::significant_drop_in_scrutinee)] - for theme_path in index.lock().list_admin_themes_paths() { - let path = theme_path.append(&[mod_name, "templates", template]); - if let Some(location) = path_to_location(&path) { - result.push(location); - } - } -} - -fn add_phtml_in_front_theme_location( - index: &ArcIndexer, - result: &mut Vec, - mod_name: &str, - template: &str, -) { - #[allow(clippy::significant_drop_in_scrutinee)] - for theme_path in index.lock().list_front_themes_paths() { - let path = theme_path.append(&[mod_name, "templates", template]); - if let Some(location) = path_to_location(&path) { - result.push(location); - } - } -} - -fn get_php_class_from_class_name(index: &MutexGuard, class: &str) -> Option { - let module_path = index.split_class_to_path_and_suffix(class); - match module_path { - None => None, - Some((mut file_path, suffix)) => { - for part in suffix { - file_path.push(part); - } - file_path.set_extension("php"); - - match file_path.try_exists() { - Ok(true) => php::parse_php_file(&file_path), - _ => None, - } - } - } + let item = state.get_item_from_position(&path, pos)?; + Some(match item { + M2Item::ModComponent(mod_name, file_path, mod_path) => { + component::mod_location(state, mod_name, &file_path, mod_path, &path) + } + M2Item::RelComponent(comp, path) => component::find_rel(comp, &path)?, + M2Item::Component(comp) => component::find_plain(state, &comp), + M2Item::AdminPhtml(mod_name, template) => phtml::find_admin(state, &mod_name, &template), + M2Item::FrontPhtml(mod_name, template) => phtml::find_front(state, &mod_name, &template), + M2Item::BasePhtml(mod_name, template) => phtml::find_base(state, &mod_name, &template), + M2Item::Class(class) => vec![php::find_class(state, &class)?], + M2Item::Method(class, method) => vec![php::find_method(state, &class, &method)?], + M2Item::Const(class, constant) => vec![php::find_const(state, &class, &constant)?], + }) } fn path_to_location(path: &Path) -> Option { diff --git a/src/lsp/definition/component.rs b/src/lsp/definition/component.rs new file mode 100644 index 0000000..55aa3d8 --- /dev/null +++ b/src/lsp/definition/component.rs @@ -0,0 +1,60 @@ +use std::path::{Path, PathBuf}; + +use lsp_types::Location; + +use crate::{ + m2::{M2Item, M2Path}, + state::State, +}; + +use super::path_to_location; + +pub fn find_plain(state: &State, comp: &str) -> Vec { + let mut result = vec![]; + let workspace_paths = state.workspace_paths(); + for path in workspace_paths { + let path = path.append(&["lib", "web", comp]).append_ext("js"); + if let Some(location) = path_to_location(&path) { + result.push(location); + } + } + result +} +pub fn find_rel(comp: String, path: &Path) -> Option> { + let mut path = path.join(comp); + path.set_extension("js"); + path_to_location(&path).map(|location| vec![location]) +} + +pub fn mod_location( + state: &State, + mod_name: String, + file_path: &str, + mod_path: PathBuf, + path: &PathBuf, +) -> Vec { + let mut result = vec![]; + let mut components = vec![M2Item::ModComponent( + mod_name.clone(), + file_path.to_string(), + mod_path, + )]; + + let area = path.get_area(); + components.extend(state.get_component_mixins_for_area(mod_name + "/" + file_path, &area)); + + for component in components { + if let M2Item::ModComponent(_, file_path, mod_path) = component { + for area_path in area.path_candidates() { + let comp_path = mod_path + .append(&["view", area_path, "web", &file_path]) + .append_ext("js"); + if let Some(location) = path_to_location(&comp_path) { + result.push(location); + } + } + } + } + + result +} diff --git a/src/lsp/definition/php.rs b/src/lsp/definition/php.rs new file mode 100644 index 0000000..ad074a4 --- /dev/null +++ b/src/lsp/definition/php.rs @@ -0,0 +1,54 @@ +use lsp_types::Location; + +use crate::{ + php::{parse_php_file, PHPClass}, + state::State, +}; + +pub fn find_class(state: &State, class: &str) -> Option { + let phpclass = get_php_class_from_class_name(state, class)?; + Some(Location { + uri: phpclass.uri.clone(), + range: phpclass.range, + }) +} + +pub fn find_method(state: &State, class: &str, method: &str) -> Option { + let phpclass = get_php_class_from_class_name(state, class)?; + Some(Location { + uri: phpclass.uri.clone(), + range: phpclass + .methods + .get(method) + .map_or(phpclass.range, |method| method.range), + }) +} + +pub fn find_const(state: &State, class: &str, constant: &str) -> Option { + let phpclass = get_php_class_from_class_name(state, class)?; + Some(Location { + uri: phpclass.uri.clone(), + range: phpclass + .constants + .get(constant) + .map_or(phpclass.range, |method| method.range), + }) +} + +fn get_php_class_from_class_name(state: &State, class: &str) -> Option { + let module_path = state.split_class_to_path_and_suffix(class); + match module_path { + None => None, + Some((mut file_path, suffix)) => { + for part in suffix { + file_path.push(part); + } + file_path.set_extension("php"); + + match file_path.try_exists() { + Ok(true) => parse_php_file(&file_path), + _ => None, + } + } + } +} diff --git a/src/lsp/definition/phtml.rs b/src/lsp/definition/phtml.rs new file mode 100644 index 0000000..4758be6 --- /dev/null +++ b/src/lsp/definition/phtml.rs @@ -0,0 +1,78 @@ +use lsp_types::Location; + +use crate::{ + m2::{M2Area, M2Path}, + state::State, +}; + +use super::path_to_location; + +pub fn find_admin(state: &State, mod_name: &str, template: &str) -> Vec { + let mut result = vec![]; + add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Adminhtml); + add_phtml_in_admin_theme_location(state, &mut result, mod_name, template); + result +} + +pub fn find_front(state: &State, mod_name: &str, template: &str) -> Vec { + let mut result = vec![]; + add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Frontend); + add_phtml_in_front_theme_location(state, &mut result, mod_name, template); + result +} + +pub fn find_base(state: &State, mod_name: &str, template: &str) -> Vec { + let mut result = vec![]; + add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Base); + add_phtml_in_front_theme_location(state, &mut result, mod_name, template); + add_phtml_in_admin_theme_location(state, &mut result, mod_name, template); + result +} + +fn add_phtml_in_mod_location( + state: &State, + result: &mut Vec, + mod_name: &str, + template: &str, + area: &M2Area, +) { + let mod_path = state.get_module_path(mod_name); + if let Some(path) = mod_path { + for area in area.path_candidates() { + let templ_path = path.append(&["view", area, "templates", template]); + if let Some(location) = path_to_location(&templ_path) { + result.push(location); + } + } + } +} + +fn add_phtml_in_admin_theme_location( + state: &State, + result: &mut Vec, + mod_name: &str, + template: &str, +) { + #[allow(clippy::significant_drop_in_scrutinee)] + for theme_path in state.list_admin_themes_paths() { + let path = theme_path.append(&[mod_name, "templates", template]); + if let Some(location) = path_to_location(&path) { + result.push(location); + } + } +} + +fn add_phtml_in_front_theme_location( + state: &State, + result: &mut Vec, + mod_name: &str, + template: &str, +) { + #[allow(clippy::significant_drop_in_scrutinee)] + for theme_path in state.list_front_themes_paths() { + let path = theme_path.append(&[mod_name, "templates", template]); + if let Some(location) = path_to_location(&path) { + result.push(location); + } + } +} diff --git a/src/m2.rs b/src/m2.rs index d88a1d7..8dce6e1 100644 --- a/src/m2.rs +++ b/src/m2.rs @@ -25,15 +25,11 @@ pub enum M2Area { } impl M2Area { - pub fn path_candidates(&self) -> Vec { + pub fn path_candidates(&self) -> Vec<&str> { match self { - Self::Frontend => vec!["frontend".to_string(), "base".to_string()], - Self::Adminhtml => vec!["adminhtml".to_string(), "base".to_string()], - Self::Base => vec![ - "frontend".to_string(), - "adminhtml".to_string(), - "base".to_string(), - ], + Self::Frontend => vec!["frontend", "base"], + Self::Adminhtml => vec!["adminhtml", "base"], + Self::Base => vec!["frontend", "adminhtml", "base"], } } @@ -45,16 +41,6 @@ impl M2Area { } } -impl ToString for M2Area { - fn to_string(&self) -> String { - match self { - Self::Frontend => "frontend".to_string(), - Self::Adminhtml => "adminhtml".to_string(), - Self::Base => "base".to_string(), - } - } -} - #[allow(clippy::module_name_repetitions)] pub trait M2Uri { fn to_path_buf(&self) -> PathBuf; @@ -63,7 +49,7 @@ pub trait M2Uri { #[allow(clippy::module_name_repetitions)] pub trait M2Path { fn has_components(&self, parts: &[&str]) -> bool; - fn string_components(&self) -> Vec; + fn str_components(&self) -> Vec<&str>; fn relative_to>(&self, base: P) -> PathBuf; fn append(&self, parts: &[&str]) -> Self; fn append_ext(&self, ext: &str) -> Self; @@ -71,7 +57,7 @@ pub trait M2Path { fn is_frontend(&self) -> bool; fn is_test(&self) -> bool; fn get_area(&self) -> M2Area; - fn to_path_string(&self) -> String; + fn to_path_str(&self) -> &str; } impl M2Path for PathBuf { @@ -103,10 +89,9 @@ impl M2Path for PathBuf { self.strip_prefix(base).unwrap_or(self).to_path_buf() } - fn to_path_string(&self) -> String { + fn to_path_str(&self) -> &str { self.to_str() .expect("PathBuf should convert to path String") - .to_string() } fn get_area(&self) -> M2Area { @@ -125,9 +110,9 @@ impl M2Path for PathBuf { } } - fn string_components(&self) -> Vec { + fn str_components(&self) -> Vec<&str> { self.components() - .map(|c| c.as_os_str().to_str().unwrap_or_default().to_string()) + .map(|c| c.as_os_str().to_str().unwrap_or_default()) .collect() } diff --git a/src/main.rs b/src/main.rs index a18fdb3..a9122ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ -mod indexer; mod js; mod lsp; mod m2; mod php; mod queries; +mod state; mod ts; mod xml; @@ -19,7 +19,10 @@ use lsp_types::{ WorkDoneProgressOptions, }; -use crate::{indexer::Indexer, m2::M2Uri}; +use crate::{ + m2::{M2Path, M2Uri}, + state::State, +}; fn main() -> Result<(), Box> { // Note that we must have our logging only write out to stderr. @@ -35,12 +38,12 @@ fn main() -> Result<(), Box> { completion_provider: Some(CompletionOptions { resolve_provider: Some(false), trigger_characters: Some(vec![ - ">".to_string(), - "\"".to_string(), - "'".to_string(), - ":".to_string(), - "\\".to_string(), - "/".to_string(), + String::from(">"), + String::from("\""), + String::from("'"), + String::from(":"), + String::from("\\"), + String::from("/"), ]), work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, @@ -78,18 +81,18 @@ fn main_loop( let params: InitializeParams = serde_json::from_value(init_params).context("Deserializing initialize params")?; - let indexer = Indexer::new().into_arc(); + let state = State::new().into_arc(); let mut threads = vec![]; if let Some(uri) = params.root_uri { let path = uri.to_file_path().expect("Invalid root path"); - threads.extend(Indexer::update_index(&indexer, &path)); + threads.extend(State::update_index(&state, &path)); }; if let Some(folders) = params.workspace_folders { for folder in folders { let path = folder.uri.to_file_path().expect("Invalid workspace path"); - threads.extend(Indexer::update_index(&indexer, &path)); + threads.extend(State::update_index(&state, &path)); } } @@ -105,12 +108,12 @@ fn main_loop( match req.method.as_str() { "textDocument/completion" => { let (id, params) = cast::(req)?; - let result = lsp::completion_handler(&indexer, ¶ms); + let result = lsp::completion_handler(&state.lock(), ¶ms); connection.sender.send(get_response_message(id, result))?; } "textDocument/definition" => { let (id, params) = cast::(req)?; - let result = lsp::definition_handler(&indexer, ¶ms); + let result = lsp::definition_handler(&state.lock(), ¶ms); connection.sender.send(get_response_message(id, result))?; } _ => { @@ -127,7 +130,7 @@ fn main_loop( let params: DidOpenTextDocumentParams = serde_json::from_value(not.params) .context("Deserializing notification params")?; let path = params.text_document.uri.to_path_buf(); - indexer.lock().set_file(&path, params.text_document.text); + state.lock().set_file(&path, params.text_document.text); #[cfg(debug_assertions)] eprintln!("textDocument/didOpen: {path:?}"); } @@ -135,9 +138,15 @@ fn main_loop( let params: DidChangeTextDocumentParams = serde_json::from_value(not.params) .context("Deserializing notification params")?; let path = params.text_document.uri.to_path_buf(); - indexer - .lock() - .set_file(&path, ¶ms.content_changes[0].text); + match path.get_ext().as_str() { + "js" | "xml" => state + .lock() + .set_file(&path, ¶ms.content_changes[0].text), + "php" if path.ends_with("registration.php") => state + .lock() + .set_file(&path, ¶ms.content_changes[0].text), + _ => (), + } #[cfg(debug_assertions)] eprintln!("textDocument/didChange: {path:?}"); } @@ -145,7 +154,7 @@ fn main_loop( let params: DidCloseTextDocumentParams = serde_json::from_value(not.params) .context("Deserializing notification params")?; let path = params.text_document.uri.to_path_buf(); - indexer.lock().del_file(&path); + state.lock().del_file(&path); #[cfg(debug_assertions)] eprintln!("textDocument/didClose: {path:?}"); } diff --git a/src/php.rs b/src/php.rs index 66cba98..4bbdb6e 100644 --- a/src/php.rs +++ b/src/php.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use convert_case::{Case, Casing}; use glob::glob; @@ -6,18 +9,12 @@ use lsp_types::{Position, Range, Url}; use tree_sitter::{Node, QueryCursor}; use crate::{ - indexer::ArcIndexer, m2::M2Path, queries, - ts::{get_node_text, get_range_from_node}, + state::{ArcState, State}, + ts::{self, get_range_from_node}, }; -#[derive(Debug, Clone)] -pub struct Callable { - pub class: String, - pub method: Option, -} - #[derive(Debug, Clone)] pub struct PHPClass { pub fqn: String, @@ -50,9 +47,9 @@ enum M2Module { fn register_param_to_module(param: &str) -> Option { if param.matches('/').count() == 2 { if param.starts_with("frontend") { - Some(M2Module::FrontTheme(param.to_string())) + Some(M2Module::FrontTheme(param.into())) } else { - Some(M2Module::AdminTheme(param.to_string())) + Some(M2Module::AdminTheme(param.into())) } } else if param.matches('/').count() == 1 { let mut parts = param.splitn(2, '/'); @@ -83,55 +80,93 @@ fn register_param_to_module(param: &str) -> Option { } } -pub fn update_index(index: &ArcIndexer, path: &PathBuf) { - let modules = glob(&path.append(&["**", "registration.php"]).to_path_string()) - .expect("Failed to read glob pattern"); +pub fn update_index(state: &ArcState, path: &PathBuf) { + // if current workspace is magento module + process_glob(state, &path.append(&["registration.php"])); + // if current workspace is magento installation + process_glob( + state, + &path.append(&["vendor", "*", "*", "registration.php"]), + ); // vendor modules / themes + process_glob( + state, + &path.append(&["app", "code", "*", "*", "registration.php"]), + ); // local modules + process_glob( + state, + &path.append(&["app", "design", "*", "*", "*", "registration.php"]), + ); // local themes + process_glob( + state, + &path.append(&[ + "vendor", + "magento", + "magento2-base", + "setup", + "src", + "Magento", + "Setup", + "registration.php", + ]), + ); // magento2-base setup module +} +pub fn maybe_index_file(state: &mut State, content: &str, file_path: &PathBuf) { + if file_path.to_path_str().ends_with("registration.php") { + update_index_from_registration(state, content, file_path); + } +} + +fn update_index_from_registration(state: &mut State, content: &str, file_path: &Path) { + state.set_source_file(file_path); let query = queries::php_registration(); - for moule_registration in modules { - moule_registration.map_or_else( - |_e| panic!("buhu"), - |file_path| { - if file_path.is_test() { - return; - } - - let content = std::fs::read_to_string(&file_path) - .expect("Should have been able to read the file"); - - let tree = tree_sitter_parsers::parse(&content, "php"); - let mut cursor = QueryCursor::new(); - let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); - for m in matches { - let mod_name = get_node_text(m.captures[1].node, &content); - let mod_name = mod_name.trim_matches('"').trim_matches('\''); - - let mut parent = file_path.clone(); - parent.pop(); - - index.lock().add_module_path(mod_name, parent.clone()); - - match register_param_to_module(mod_name) { - Some(M2Module::Module(m)) => { - index - .lock() - .add_module(mod_name) - .add_module_path(&m, parent); - } - Some(M2Module::Library(l)) => { - index.lock().add_module_path(&l, parent); - } - Some(M2Module::FrontTheme(t)) => { - index.lock().add_front_theme_path(&t, parent); - } - Some(M2Module::AdminTheme(t)) => { - index.lock().add_admin_theme_path(&t, parent); - } - _ => (), - } - } - }, - ); + let tree = tree_sitter_parsers::parse(content, "php"); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); + for m in matches { + let mod_name = ts::get_node_str(m.captures[1].node, content) + .trim_matches('"') + .trim_matches('\''); + + let mut parent = file_path.to_path_buf(); + parent.pop(); + + state.add_module_path(mod_name, parent.clone()); + + match register_param_to_module(mod_name) { + Some(M2Module::Module(m)) => { + state.add_module(mod_name).add_module_path(m, parent); + } + Some(M2Module::Library(l)) => { + state + .add_module(&l.replace('\\', "_")) + .add_module_path(l, parent); + } + Some(M2Module::FrontTheme(t)) => { + state.add_front_theme_path(t, parent); + } + Some(M2Module::AdminTheme(t)) => { + state.add_admin_theme_path(t, parent); + } + _ => (), + } + } +} + +fn process_glob(state: &ArcState, glob_path: &PathBuf) { + let modules = glob(glob_path.to_path_str()) + .expect("Failed to read glob pattern") + .filter_map(Result::ok); + + for file_path in modules { + if file_path.is_test() { + return; + } + + let content = + std::fs::read_to_string(&file_path).expect("Should have been able to read the file"); + + update_index_from_registration(&mut state.lock(), &content, &file_path); } } @@ -158,12 +193,12 @@ pub fn parse_php_file(file_path: &PathBuf) -> Option { } if m.pattern_index == 3 { let method_node = m.captures[1].node; - let method_name = method_node.utf8_text(content.as_bytes()).unwrap_or(""); + let method_name = ts::get_node_str(method_node, &content); if !method_name.is_empty() { methods.insert( - method_name.to_string(), + method_name.into(), PHPMethod { - name: method_name.to_string(), + name: method_name.into(), range: get_range_from_node(method_node), }, ); @@ -174,9 +209,9 @@ pub fn parse_php_file(file_path: &PathBuf) -> Option { let const_name = const_node.utf8_text(content.as_bytes()).unwrap_or(""); if !const_name.is_empty() { constants.insert( - const_name.to_string(), + const_name.into(), PHPConst { - name: const_name.to_string(), + name: const_name.into(), range: get_range_from_node(const_node), }, ); diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..cf9c92c --- /dev/null +++ b/src/state.rs @@ -0,0 +1,362 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, + thread::{spawn, JoinHandle}, + time::SystemTime, +}; + +use lsp_types::Position; +use parking_lot::Mutex; + +use crate::{ + js, + m2::{M2Area, M2Item, M2Path}, + php, xml, +}; + +trait HashMapId { + fn id(&self) -> usize; +} + +impl HashMapId for M2Area { + fn id(&self) -> usize { + match self { + Self::Frontend => 0, + Self::Adminhtml => 1, + Self::Base => 2, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Trackee { + Module(String), + ModulePath(String), + JsMap(M2Area, String), + JsMixin(M2Area, String), + Themes(M2Area, String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TrackingList(HashMap>); + +impl TrackingList { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn track(&mut self, source_path: &Path, trackee: Trackee) { + self.0 + .entry(source_path.into()) + .or_insert_with(Vec::new) + .push(trackee); + } + + pub fn maybe_track(&mut self, source_path: Option<&PathBuf>, trackee: Trackee) { + if let Some(source_path) = source_path { + self.track(source_path, trackee); + } + } + + pub fn untrack(&mut self, source_path: &Path) -> Option> { + self.0.remove(source_path) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct State { + source_file: Option, + track_entities: TrackingList, + buffers: HashMap, + modules: Vec, + module_paths: HashMap, + front_themes: HashMap, + admin_themes: HashMap, + js_maps: [HashMap; 3], + js_mixins: [HashMap>; 3], + workspaces: Vec, +} + +#[allow(clippy::module_name_repetitions)] +pub type ArcState = Arc>; + +impl State { + pub fn new() -> Self { + Self { + source_file: None, + track_entities: TrackingList::new(), + buffers: HashMap::new(), + modules: vec![], + module_paths: HashMap::new(), + front_themes: HashMap::new(), + admin_themes: HashMap::new(), + js_maps: [HashMap::new(), HashMap::new(), HashMap::new()], + js_mixins: [HashMap::new(), HashMap::new(), HashMap::new()], + workspaces: vec![], + } + } + + pub fn set_source_file(&mut self, path: &Path) { + self.source_file = Some(path.to_owned()); + } + + pub fn clear_from_source(&mut self, path: &Path) { + if let Some(list) = self.track_entities.untrack(path) { + for trackee in list { + match trackee { + Trackee::JsMap(area, name) => { + self.js_maps[area.id()].remove(&name); + } + Trackee::JsMixin(area, name) => { + self.js_mixins[area.id()].remove(&name); + } + Trackee::Module(module) => { + self.modules.retain(|m| m != &module); + } + Trackee::ModulePath(module) => { + self.module_paths.remove(&module); + } + Trackee::Themes(area, module) => match area { + M2Area::Frontend => { + self.front_themes.remove(&module); + } + M2Area::Adminhtml => { + self.admin_themes.remove(&module); + } + M2Area::Base => { + self.front_themes.remove(&module); + self.admin_themes.remove(&module); + } + }, + } + } + } + } + + pub fn set_file(&mut self, path: &Path, content: S) + where + S: Into, + { + let content = content.into(); + self.clear_from_source(path); + js::maybe_index_file(self, &content, &path.to_owned()); + php::maybe_index_file(self, &content, &path.to_owned()); + + self.buffers.insert(path.to_owned(), content); + } + + pub fn get_file(&self, path: &PathBuf) -> Option<&String> { + self.buffers.get(path) + } + + pub fn del_file(&mut self, path: &PathBuf) { + self.buffers.remove(path); + } + + pub fn get_modules(&self) -> Vec { + let mut modules = self.modules.clone(); + modules.sort_unstable(); + modules.dedup(); + modules + } + + pub fn get_module_class_prefixes(&self) -> Vec { + self.get_modules() + .iter() + .map(|m| m.replace('_', "\\")) + .collect() + } + + pub fn get_module_path(&self, module: &str) -> Option { + self.module_paths.get(module).cloned() + } + + pub fn add_module(&mut self, module: &str) -> &mut Self { + self.track_entities + .maybe_track(self.source_file.as_ref(), Trackee::Module(module.into())); + + self.modules.push(module.into()); + self + } + + pub fn add_module_path(&mut self, module: S, path: PathBuf) -> &mut Self + where + S: Into, + { + let module = module.into(); + self.track_entities.maybe_track( + self.source_file.as_ref(), + Trackee::ModulePath(module.clone()), + ); + + self.module_paths.insert(module, path); + self + } + + pub fn add_admin_theme_path(&mut self, name: S, path: PathBuf) + where + S: Into, + { + let name = name.into(); + self.track_entities.maybe_track( + self.source_file.as_ref(), + Trackee::Themes(M2Area::Adminhtml, name.clone()), + ); + + self.admin_themes.insert(name, path); + } + + pub fn add_front_theme_path(&mut self, name: S, path: PathBuf) + where + S: Into, + { + let name = name.into(); + self.track_entities.maybe_track( + self.source_file.as_ref(), + Trackee::Themes(M2Area::Frontend, name.clone()), + ); + + self.front_themes.insert(name, path); + } + + pub fn get_component_map(&self, name: &str, area: &M2Area) -> Option<&String> { + self.js_maps[area.id()].get(name) + } + + pub fn get_component_maps_for_area(&self, area: &M2Area) -> Vec { + self.js_maps[area.id()] + .keys() + .map(ToString::to_string) + .collect() + } + + pub fn add_component_map(&mut self, name: S, val: S, area: &M2Area) + where + S: Into, + { + let name = name.into(); + self.track_entities.maybe_track( + self.source_file.as_ref(), + Trackee::JsMap(area.clone(), name.clone()), + ); + + self.js_maps[area.id()].insert(name, val.into()); + } + + pub fn add_component_mixin(&mut self, name: S, val: S, area: &M2Area) + where + S: Into, + { + let name = name.into(); + let val = val.into(); + + self.track_entities.maybe_track( + self.source_file.as_ref(), + Trackee::JsMixin(area.clone(), name.clone()), + ); + + self.js_mixins[area.id()] + .entry(name) + .or_insert_with(Vec::new) + .push(val); + } + + pub fn get_component_mixins_for_area(&self, name: S, area: &M2Area) -> Vec + where + S: Into, + { + let empty_path = Path::new(""); + self.js_mixins[area.id()] + .get(&name.into()) + .unwrap_or(&vec![]) + .iter() + .filter_map(|mod_string| js::text_to_component(self, mod_string, empty_path)) + .collect() + } + + pub fn list_front_themes_paths(&self) -> Vec<&PathBuf> { + self.front_themes.values().collect::>() + } + + pub fn list_admin_themes_paths(&self) -> Vec<&PathBuf> { + self.admin_themes.values().collect::>() + } + + pub fn workspace_paths(&self) -> Vec { + self.workspaces.clone() + } + + pub fn add_workspace_path(&mut self, path: &Path) { + self.workspaces.push(path.to_path_buf()); + } + + pub fn has_workspace_path(&mut self, path: &Path) -> bool { + self.workspaces.contains(&path.to_path_buf()) + } + + pub fn get_item_from_position(&self, path: &PathBuf, pos: Position) -> Option { + match path.get_ext().as_str() { + "js" => js::get_item_from_position(self, path, pos), + "xml" => xml::get_item_from_position(self, path, pos), + _ => None, + } + } + + pub fn into_arc(self) -> ArcState { + Arc::new(Mutex::new(self)) + } + + pub fn update_index(arc_state: &ArcState, path: &Path) -> Vec> { + let mut state = arc_state.lock(); + if state.has_workspace_path(path) { + vec![] + } else { + state.add_workspace_path(path); + vec![ + spawn_index(arc_state, path, php::update_index, "PHP Indexing"), + spawn_index(arc_state, path, js::update_index, "JS Indexing"), + ] + } + } + + pub fn split_class_to_path_and_suffix(&self, class: &str) -> Option<(PathBuf, Vec)> { + let mut parts = class.split('\\').collect::>(); + let mut suffix = vec![]; + + while let Some(part) = parts.pop() { + suffix.push(part.to_string()); + let prefix = parts.join("\\"); + let module_path = self.get_module_path(&prefix); + match module_path { + Some(mod_path) => { + suffix.reverse(); + return Some((mod_path, suffix)); + } + None => continue, + } + } + None + } +} + +fn spawn_index( + state: &ArcState, + path: &Path, + callback: fn(&ArcState, &PathBuf), + msg: &str, +) -> JoinHandle<()> { + let state = Arc::clone(state); + let path = path.to_path_buf(); + let msg = msg.to_owned(); + + spawn(move || { + eprintln!("Start {}", msg); + let index_start = SystemTime::now(); + callback(&state, &path); + index_start.elapsed().map_or_else( + |_| eprintln!("{} done", msg), + |d| eprintln!("{} done in {:?}", msg, d), + ); + }) +} diff --git a/src/ts.rs b/src/ts.rs index 618dfa5..0496914 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -15,17 +15,13 @@ pub fn get_range_from_node(node: Node) -> Range { } pub fn get_node_text_before_pos(node: Node, content: &str, pos: Position) -> String { - let text = node - .utf8_text(content.as_bytes()) - .unwrap_or("") - .trim_matches('\\') - .to_string(); + let text = node.utf8_text(content.as_bytes()).unwrap_or(""); let node_start_pos = node.start_position(); let node_end_pos = node.end_position(); let text = if node_end_pos.row == node_start_pos.row { - text + text.to_string() } else { let take_lines = pos.line as usize - node_start_pos.row; text.split('\n') @@ -59,11 +55,10 @@ pub fn get_node_text_before_pos(node: Node, content: &str, pos: Position) -> Str } } -pub fn get_node_text(node: Node, content: &str) -> String { +pub fn get_node_str<'a>(node: Node, content: &'a str) -> &'a str { node.utf8_text(content.as_bytes()) .unwrap_or("") .trim_matches('\\') - .to_string() } pub fn node_at_position(node: Node, pos: Position) -> bool { diff --git a/src/xml.rs b/src/xml.rs index 5a1be6e..03909f3 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -6,11 +6,11 @@ use std::{ use tree_sitter::{Node, QueryCursor}; use crate::{ - indexer::Indexer, js, m2::{M2Area, M2Item, M2Path}, queries, - ts::{get_node_text, get_node_text_before_pos, node_at_position, node_last_child}, + state::State, + ts::{get_node_str, get_node_text_before_pos, node_at_position, node_last_child}, }; #[allow(clippy::module_name_repetitions)] @@ -143,12 +143,12 @@ fn node_to_tag(node: Node, content: &str) -> Option { while let Some(node) = node_walk_back(current_node) { current_node = node; if node.kind() == "self_closing_tag" || node.kind() == "start_tag" { - let text = get_node_text(node, content); + let text = get_node_str(node, content); if text.chars().last()? != '>' { return None; } return get_xml_tag_at_pos( - &text, + text, Position { line: 0, character: 0, @@ -166,14 +166,14 @@ fn node_to_path(node: Node, content: &str) -> Option { let mut node_ids = vec![]; let mut on_text_node = false; let mut pop_last = false; - let text = get_node_text(node, content); + let text = get_node_str(node, content); if node.kind() == ">" && text == ">" { on_text_node = true; } if node.kind() == "text" && node.prev_sibling().is_some() { if let Some(last) = node_last_child(node.prev_sibling()?) { - if last.kind() == ">" && get_node_text(last, content) == ">" { + if last.kind() == ">" && get_node_str(last, content) == ">" { on_text_node = true; } } @@ -186,7 +186,7 @@ fn node_to_path(node: Node, content: &str) -> Option { } node_ids.push(node.id()); if node.kind() == "attribute_name" && !has_attr { - let attr_name = get_node_text(node, content); + let attr_name = get_node_str(node, content); has_attr = true; path.push((node.kind(), attr_name)); } else if node.kind() == "self_closing_tag" || node.kind() == "start_tag" { @@ -194,10 +194,10 @@ fn node_to_path(node: Node, content: &str) -> Option { if node_ids.contains(&node.child(0)?.id()) { continue; } - path.push((node.kind(), get_node_text(node.child(1)?, content))); + path.push((node.kind(), get_node_str(node.child(1)?, content))); } } else if node.kind() == "tag_name" && node.parent()?.kind() != "end_tag" { - path.push((node.kind(), get_node_text(node, content))); + path.push((node.kind(), get_node_str(node, content))); } else if node.kind() == "tag_name" && node.parent()?.kind() == "end_tag" { pop_last = true; on_text_node = false; @@ -208,19 +208,19 @@ fn node_to_path(node: Node, content: &str) -> Option { path.pop(); } if on_text_node { - path.push(("text", "[$text]".into())); + path.push(("text", "[$text]")); } let mut result = String::new(); for (kind, name) in path { match kind { - "text" => result.push_str(&name), + "text" => result.push_str(name), "attribute_name" => { result.push_str("[@"); - result.push_str(&name); + result.push_str(name); result.push(']'); } "self_closing_tag" | "start_tag" | "tag_name" => { - result.push_str(&format!("/{}", &name)); + result.push_str(&format!("/{}", name)); } _ => (), @@ -229,13 +229,13 @@ fn node_to_path(node: Node, content: &str) -> Option { Some(result) } -pub fn get_item_from_position(index: &Indexer, path: &PathBuf, pos: Position) -> Option { - let content = index.get_file(path)?; - get_item_from_pos(index, content, path, pos) +pub fn get_item_from_position(state: &State, path: &PathBuf, pos: Position) -> Option { + let content = state.get_file(path)?; + get_item_from_pos(state, content, path, pos) } fn get_item_from_pos( - index: &Indexer, + state: &State, content: &str, path: &PathBuf, pos: Position, @@ -259,9 +259,9 @@ fn get_item_from_pos( "object" => Some(get_class_item_from_str(text)), "init_parameter" => try_const_item_from_str(text), "string" => { - if tag.attributes.get("name") == Some(&"component".to_string()) { - let text = js::resolve_component_text(index, text, &path.get_area())?; - js::text_to_component(index, text, path) + if tag.attributes.get("name").is_some_and(|s| s == "component") { + let text = js::resolve_component_text(state, text, &path.get_area())?; + js::text_to_component(state, text, path) } else { try_any_item_from_str(text, &path.get_area()) } @@ -280,7 +280,7 @@ fn get_xml_tag_at_pos(content: &str, pos: Position) -> Option { let mut cursor = QueryCursor::new(); let captures = cursor.captures(query, tree.root_node(), content.as_bytes()); - let mut last_attribute_name = String::new(); + let mut last_attribute_name = ""; let mut last_tag_id: Option = None; let mut tag = XmlTag::new(); @@ -299,22 +299,24 @@ fn get_xml_tag_at_pos(content: &str, pos: Position) -> Option { let hovered = node_at_position(node, pos); match node.kind() { "tag_name" => { - tag.name = get_node_text(node, content); + tag.name = get_node_str(node, content).into(); } "attribute_name" => { - last_attribute_name = get_node_text(node, content); + last_attribute_name = get_node_str(node, content); tag.attributes - .insert(last_attribute_name.clone(), String::new()); + .insert(last_attribute_name.into(), String::new()); } "attribute_value" => { - tag.attributes - .insert(last_attribute_name.clone(), get_node_text(node, content)); + tag.attributes.insert( + last_attribute_name.into(), + get_node_str(node, content).into(), + ); if hovered { - tag.hover_on = XmlPart::Attribute(last_attribute_name.clone()); + tag.hover_on = XmlPart::Attribute(last_attribute_name.into()); } } "text" => { - tag.text = get_node_text(node, content); + tag.text = get_node_str(node, content).into(); if hovered { tag.hover_on = XmlPart::Text; } @@ -345,17 +347,14 @@ fn try_any_item_from_str(text: &str, area: &M2Area) -> Option { fn try_const_item_from_str(text: &str) -> Option { if text.split("::").count() == 2 { let mut parts = text.split("::"); - Some(M2Item::Const( - parts.next()?.to_string(), - parts.next()?.to_string(), - )) + Some(M2Item::Const(parts.next()?.into(), parts.next()?.into())) } else { None } } fn get_class_item_from_str(text: &str) -> M2Item { - M2Item::Class(text.to_string()) + M2Item::Class(text.into()) } fn try_phtml_item_from_str(text: &str, area: &M2Area) -> Option { @@ -363,16 +362,16 @@ fn try_phtml_item_from_str(text: &str, area: &M2Area) -> Option { let mut parts = text.split("::"); match area { M2Area::Frontend => Some(M2Item::FrontPhtml( - parts.next()?.to_string(), - parts.next()?.to_string(), + parts.next()?.into(), + parts.next()?.into(), )), M2Area::Adminhtml => Some(M2Item::AdminPhtml( - parts.next()?.to_string(), - parts.next()?.to_string(), + parts.next()?.into(), + parts.next()?.into(), )), M2Area::Base => Some(M2Item::BasePhtml( - parts.next()?.to_string(), - parts.next()?.to_string(), + parts.next()?.into(), + parts.next()?.into(), )), } } else { @@ -383,13 +382,13 @@ fn try_phtml_item_from_str(text: &str, area: &M2Area) -> Option { fn try_method_item_from_tag(tag: &XmlTag) -> Option { if tag.attributes.get("instance").is_some() && tag.attributes.get("method").is_some() { Some(M2Item::Method( - tag.attributes.get("instance")?.to_string(), - tag.attributes.get("method")?.to_string(), + tag.attributes.get("instance")?.into(), + tag.attributes.get("method")?.into(), )) } else if tag.attributes.get("class").is_some() && tag.attributes.get("method").is_some() { Some(M2Item::Method( - tag.attributes.get("class")?.to_string(), - tag.attributes.get("method")?.to_string(), + tag.attributes.get("class")?.into(), + tag.attributes.get("method")?.into(), )) } else { None @@ -429,8 +428,8 @@ mod test { let win_path = format!("c:{}", path.replace('/', "\\")); let pos = get_position_from_test_xml(xml); let uri = PathBuf::from(if cfg!(windows) { &win_path } else { path }); - let index = Indexer::new(); - get_item_from_pos(&index, &xml.replace('|', ""), &uri, pos) + let state = State::new(); + get_item_from_pos(&state, &xml.replace('|', ""), &uri, pos) } fn get_test_xml_tag_at_pos(xml: &str) -> Option { @@ -442,7 +441,7 @@ mod test { fn test_get_item_from_pos_class_in_tag_text() { let item = get_test_item_from_pos(r#"|A\B\C"#, "/a/b/c"); - assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); } #[test] @@ -454,8 +453,8 @@ mod test { assert_eq!( item, Some(M2Item::AdminPhtml( - "Some_Module".to_string(), - "path/to/file.phtml".to_string() + "Some_Module".into(), + "path/to/file.phtml".into() )) ); } @@ -469,8 +468,8 @@ mod test { assert_eq!( item, Some(M2Item::FrontPhtml( - "Some_Module".to_string(), - "path/to/file.phtml".to_string() + "Some_Module".into(), + "path/to/file.phtml".into() )) ); } @@ -483,7 +482,7 @@ mod test { ); assert_eq!( item, - Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) ); } @@ -495,7 +494,7 @@ mod test { ); assert_eq!( item, - Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) ); } @@ -507,7 +506,7 @@ mod test { ); assert_eq!( item, - Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) ); } @@ -517,13 +516,13 @@ mod test { r#"xx"#, "/a/a/c", ); - assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); } #[test] fn test_get_item_from_pos_class_in_text_in_tag() { let item = get_test_item_from_pos(r#"|A\B\C"#, "/a/a/c"); - assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); } #[test] @@ -534,10 +533,7 @@ mod test { ); assert_eq!( item, - Some(M2Item::Const( - "A\\B\\C".to_string(), - "CONST_ANT".to_string() - )) + Some(M2Item::Const("A\\B\\C".into(), "CONST_ANT".into())) ); } @@ -550,8 +546,8 @@ mod test { assert_eq!( item, Some(M2Item::AdminPhtml( - "Some_Module".to_string(), - "file.phtml".to_string() + "Some_Module".into(), + "file.phtml".into() )) ); } @@ -582,7 +578,7 @@ mod test { "#, "/a/a/c", ); - assert_eq!(item, Some(M2Item::Class("Some\\Class\\Name".to_string()))) + assert_eq!(item, Some(M2Item::Class("Some\\Class\\Name".into()))) } #[test] @@ -593,7 +589,7 @@ mod test { "#, "/a/a/c", ); - assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))) + assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))) } #[test]