From 53aaecee3a6c8ae12a1efd708d7d1b278c730e2d Mon Sep 17 00:00:00 2001 From: Pawel Bogut Date: Mon, 25 Sep 2023 23:05:18 +0200 Subject: [PATCH] feat: add initial js support --- src/js.rs | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 241 +++++++++++++++++++++++-------------- src/php.rs | 16 +-- 3 files changed, 501 insertions(+), 97 deletions(-) create mode 100644 src/js.rs diff --git a/src/js.rs b/src/js.rs new file mode 100644 index 0000000..784642c --- /dev/null +++ b/src/js.rs @@ -0,0 +1,341 @@ +use std::path::{Path, PathBuf}; + +use glob::glob; +use lsp_types::{Position, Url}; +use tree_sitter::{Node, Query, QueryCursor}; + +use crate::{ts::node_at_position, Indexer}; + +enum JSTypes { + Map, + Paths, + Mixins, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum M2Item { + Component(String), + ModComponent(String, String, PathBuf), + RelComponent(String, PathBuf), +} + +pub fn update_index(index: &mut Indexer, root_path: &Path) { + let modules = glob( + root_path + .join("**/requirejs-config.js") + .to_str() + .expect("Path should be in valid encoding"), + ) + .expect("Failed to read glob pattern"); + + 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"); + + update_index_from_config(index, &content); + }, + ); + } +} + +pub fn make_web_uris(root: &Path, path: &Path) -> Vec { + let mut result = vec![]; + for area in ["base", "frontend", "backend"] { + let mut maybe_file = root.join("view").join(&area).join("web").join(path); + maybe_file.set_extension("js"); + if maybe_file.exists() { + result.push(Url::from_file_path(&maybe_file).expect("Should be valid url")); + } + } + + result +} + +pub fn get_item_from_position(index: &Indexer, uri: &Url, pos: Position) -> Option { + let path = uri.to_file_path().expect("Should be valid file path"); + let path = path.to_str()?; + let content = std::fs::read_to_string(path).expect("Should have been able to read the file"); + get_item_from_pos(index, &content, uri, pos) +} + +fn get_item_from_pos(index: &Indexer, content: &str, uri: &Url, pos: Position) -> Option { + let query = r#" + ( + (identifier) @def (#eq? @def define) + (arguments (array (string) @str)) + ) + "#; + let tree = tree_sitter_parsers::parse(&content, "javascript"); + let query = Query::new(tree.language(), query) + .map_err(|e| eprintln!("Error creating query: {:?}", e)) + .expect("Error creating query"); + + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), content.as_bytes()); + + 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, uri)?; + + return text_to_component(index, text, uri); + } + } + + None +} + +fn resolve_component_text(index: &Indexer, text: &str, uri: &Url) -> Option { + match index.js_maps.get(text) { + Some(t) => resolve_component_text(index, t, uri), + None => Some(text.to_string()), + } +} + +fn text_to_component(index: &Indexer, text: String, uri: &Url) -> Option { + let begining = text.split('/').next().unwrap_or(""); + + if begining.chars().next().unwrap_or('a') == '.' { + let mut path = uri.to_file_path().expect("Should be valid file path"); + path.pop(); + Some(M2Item::RelComponent(text, 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.magento_modules.get(&mod_name)?; + Some(M2Item::ModComponent( + mod_name, + parts.next()?.to_string(), + mod_path.clone(), + )) + } else { + Some(M2Item::Component(text)) + } +} + +fn update_index_from_config(index: &mut Indexer, content: &str) { + let map_query = r#" + ( + (identifier) @config + (object (pair [(property_identifier) (string)] @mapkey + (object (pair (object (pair + [(property_identifier) (string)] @key + (string) @val + )))) + )) + + (#eq? @config config) + (#match? @mapkey "[\"']?map[\"']?") + ) + "#; + + let mixins_query = r#" + ( + (identifier) @config + (object (pair [(property_identifier) (string)] ; @configkey + (object (pair [(property_identifier) (string)] @mixins + (object (pair [(property_identifier) (string)] @key + (object (pair [(property_identifier) (string)] @val (true))) + )) + )) + )) + + (#match? @config config) + ; (#match? @configkey "[\"']?config[\"']?") + (#match? @mixins "[\"']?mixins[\"']?") + ) + "#; + + let path_query = r#" + ( + (identifier) @config + (object (pair [(property_identifier) (string)] @pathskey + (((object (pair + [(property_identifier) (string)] @key + (string) @val + )))) + )) + + (#eq? @config config) + (#match? @pathskey "[\"']?paths[\"']?") + ) + "#; + + let query = format!("{} {} {}", map_query, path_query, mixins_query); + let tree = tree_sitter_parsers::parse(content, "javascript"); + let query = Query::new(tree.language(), &query) + .map_err(|e| eprintln!("Error creating query: {:?}", e)) + .expect("Error creating query"); + + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), content.as_bytes()); + + for m in matches { + let key = get_node_text(m.captures[2].node, content); + let val = get_node_text(m.captures[3].node, content); + match get_kind(m.captures[1].node, content) { + Some(JSTypes::Map) | Some(JSTypes::Paths) => index.js_maps.insert(key, val), + Some(JSTypes::Mixins) => index.js_mixins.insert(key, val), + None => continue, + }; + } +} + +fn get_kind(node: Node, content: &str) -> Option { + match get_node_text(node, content).as_str() { + "map" => Some(JSTypes::Map), + "paths" => Some(JSTypes::Paths), + "mixins" => Some(JSTypes::Mixins), + _ => None, + } +} + +fn get_node_text(node: Node, content: &str) -> String { + let result = node + .utf8_text(content.as_bytes()) + .unwrap_or("") + .trim_matches('\\') + .to_string(); + + if node.kind() == "string" { + match get_node_text(node.child(0).unwrap_or(node), content) + .chars() + .next() + { + Some(trim) => result.trim_matches(trim).to_string(), + None => result, + } + } else { + result + } +} + +#[cfg(test)] +mod test { + use std::{collections::HashMap, path::PathBuf}; + + use super::*; + + #[test] + fn test_update_index_from_config() { + let mut index = crate::Indexer::new(); + let content = r#" + var config = { + map: { + '*': { + 'some/js/component': 'Some_Model/js/component', + otherComp: 'Some_Other/js/comp' + } + }, + "paths": { + 'other/core/extension': 'Other_Module/js/core_ext', + prototype: 'Something_Else/js/prototype.min' + }, + config: { + mixins: { + "Mage_Module/js/smth" : { + "My_Module/js/mixin/smth" : true + }, + Adobe_Module: { + "My_Module/js/mixin/adobe": true + }, + } + } + }; + "#; + + let mut m = HashMap::new(); + let mut x = HashMap::new(); + + ins(&mut m, "other/core/extension", "Other_Module/js/core_ext"); + ins(&mut m, "prototype", "Something_Else/js/prototype.min"); + ins(&mut m, "some/js/component", "Some_Model/js/component"); + ins(&mut m, "otherComp", "Some_Other/js/comp"); + ins(&mut x, "Mage_Module/js/smth", "My_Module/js/mixin/smth"); + ins(&mut x, "Adobe_Module", "My_Module/js/mixin/adobe"); + + update_index_from_config(&mut index, content); + + assert_eq!(index.js_maps, m); + assert_eq!(index.js_mixins, x); + } + + #[test] + fn get_item_from_pos_mod_component() { + let item = get_test_item( + r#" + define([ + 'Some_Module/some/vie|w', + ], function (someView) {}) + "#, + "/a/b/c", + ); + assert_eq!( + item, + Some(M2Item::ModComponent( + "Some_Module".to_string(), + "some/view".to_string(), + PathBuf::from("/a/b/c/Some_Module") + )) + ); + } + + #[test] + fn get_item_from_pos_component() { + let item = get_test_item( + r#" + define([ + 'jqu|ery', + ], function ($) {}) + "#, + "/a/b/c", + ); + assert_eq!(item, Some(M2Item::Component("jquery".to_string()))); + } + + #[test] + fn get_item_from_pos_component_with_slashes() { + let item = get_test_item( + r#" + define([ + 'jqu|ery-ui-modules/widget', + ], function (widget) {}) + "#, + "/a/b/c", + ); + assert_eq!( + item, + Some(M2Item::Component("jquery-ui-modules/widget".to_string())) + ); + } + + fn get_test_item(xml: &str, path: &str) -> Option { + let win_path = format!("c:{}", path.replace('/', "\\")); + let mut character = 0; + let mut line = 0; + for l in xml.lines() { + if l.contains('|') { + character = l.find('|').expect("Test has to have a | character") as u32; + break; + } + line += 1; + } + let pos = Position { line, character }; + let uri = Url::from_file_path(PathBuf::from(if cfg!(windows) { &win_path } else { path })) + .unwrap(); + let mut index = Indexer::new(); + index.magento_modules.insert( + "Some_Module".to_string(), + PathBuf::from("/a/b/c/Some_Module"), + ); + get_item_from_pos(&index, &xml.replace('|', ""), &uri, pos) + } + + fn ins(map: &mut HashMap, key: &str, val: &str) { + map.insert(key.to_string(), val.to_string()); + } +} diff --git a/src/main.rs b/src/main.rs index 0246465..b81974f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,10 @@ +mod js; mod php; mod ts; mod xml; + +use std::{collections::HashMap, error::Error, path::PathBuf, time::SystemTime}; + use anyhow::{Context, Result}; use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response}; use lsp_types::{ @@ -8,9 +12,6 @@ use lsp_types::{ }; use lsp_types::{GotoDefinitionParams, Location, OneOf, Range, Url}; use php::{parse_php_file, M2Item, PHPClass}; -use std::collections::HashMap; -use std::error::Error; -use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Indexer { @@ -18,6 +19,23 @@ pub struct Indexer { pub magento_modules: HashMap, pub magento_front_themes: HashMap, pub magento_admin_themes: HashMap, + pub js_maps: HashMap, + pub js_mixins: HashMap, + pub root_path: Option, +} + +impl Indexer { + pub fn new() -> Self { + Self { + php_classes: HashMap::new(), + magento_modules: HashMap::new(), + magento_front_themes: HashMap::new(), + magento_admin_themes: HashMap::new(), + js_maps: HashMap::new(), + js_mixins: HashMap::new(), + root_path: None, + } + } } fn main() -> Result<(), Box> { @@ -51,22 +69,23 @@ fn main_loop( let params: InitializeParams = serde_json::from_value(init_params).context("Deserializing initialize params")?; - let map: HashMap = HashMap::new(); + let index_start = SystemTime::now(); eprint!("Preparing index..."); + let root_uri = params.root_uri.context("Root uri is required")?; let root_path = root_uri .to_file_path() .expect("Root uri should be valid file path"); + let mut indexer = Indexer::new(); + indexer.root_path = Some(root_path.clone()); + + php::update_index(&mut indexer, &PathBuf::from(&root_path)); + js::update_index(&mut indexer, &PathBuf::from(&root_path)); - let mut indexer = Indexer { - php_classes: map, - magento_modules: HashMap::new(), - magento_front_themes: HashMap::new(), - magento_admin_themes: HashMap::new(), - }; - php::update_index(&mut indexer, &PathBuf::from(root_path)); - eprintln!(" done"); + index_start + .elapsed() + .map_or_else(|_| eprintln!(" done"), |d| eprintln!(" done in {:?}", d)); eprintln!("Starting main loop"); for msg in &connection.receiver { @@ -166,99 +185,141 @@ fn get_location_from_params( ) -> Option> { let uri = params.text_document_position_params.text_document.uri; let pos = params.text_document_position_params.position; + let file_path = uri.to_file_path().expect("Should be valid file path"); - match xml::get_item_from_position(&uri, pos) { - Some(M2Item::AdminPhtml(mod_name, template)) => { - let mut result = vec![]; - let mod_path = index.magento_modules.get(&mod_name); - if let Some(path) = mod_path { - let templ_path = path - .join("view") - .join("adminhtml") - .join("templates") - .join(&template); - if templ_path.is_file() { + match file_path.extension()?.to_str()?.to_lowercase().as_str() { + "js" => match js::get_item_from_position(&index, &uri, pos) { + Some(js::M2Item::ModComponent(_mod_name, file_path, mod_path)) => { + let mut result = vec![]; + for uri in js::make_web_uris(&mod_path, &PathBuf::from(&file_path)) { result.push(Location { - uri: Url::from_file_path(templ_path).expect("Should be valid Url"), + uri, range: Range::default(), }); } - }; - for theme_path in index.magento_admin_themes.values() { - let templ_path = theme_path.join(&mod_name).join("templates").join(&template); - if templ_path.is_file() { - result.push(Location { - uri: Url::from_file_path(templ_path).expect("Should be valid url"), + Some(result) + } + Some(js::M2Item::RelComponent(comp, path)) => { + let mut path = path.join(comp); + path.set_extension("js"); + if path.exists() { + Some(vec![Location { + uri: Url::from_file_path(path).expect("Should be valid url"), range: Range::default(), - }); + }]) + } else { + None } } - - Some(result) - } - Some(M2Item::FrontPhtml(mod_name, template)) => { - let mut result = vec![]; - let mod_path = index.magento_modules.get(&mod_name); - if let Some(path) = mod_path { - let templ_path = path - .join("view") - .join("frontend") - .join("templates") - .join(&template); - if templ_path.is_file() { - result.push(Location { - uri: Url::from_file_path(templ_path).expect("Should be valid Url"), + Some(js::M2Item::Component(comp)) => { + let mut p3 = index.root_path.clone()?.join("lib").join("web").join(&comp); + p3.set_extension("js"); + if p3.exists() { + Some(vec![Location { + uri: Url::from_file_path(p3).expect("Should be valid url"), range: Range::default(), - }); + }]) + } else { + None } - }; + } + None => None, + }, + "xml" => match xml::get_item_from_position(&uri, pos) { + Some(M2Item::AdminPhtml(mod_name, template)) => { + let mut result = vec![]; + let mod_path = index.magento_modules.get(&mod_name); + if let Some(path) = mod_path { + let templ_path = path + .join("view") + .join("adminhtml") + .join("templates") + .join(&template); + if templ_path.is_file() { + result.push(Location { + uri: Url::from_file_path(templ_path).expect("Should be valid Url"), + range: Range::default(), + }); + } + }; - for theme_path in index.magento_front_themes.values() { - let templ_path = theme_path.join(&mod_name).join("templates").join(&template); - if templ_path.is_file() { - result.push(Location { - uri: Url::from_file_path(templ_path).expect("Should be valid url"), - range: Range::default(), - }); + for theme_path in index.magento_admin_themes.values() { + let templ_path = theme_path.join(&mod_name).join("templates").join(&template); + if templ_path.is_file() { + result.push(Location { + uri: Url::from_file_path(templ_path).expect("Should be valid url"), + range: Range::default(), + }); + } } + + Some(result) } + Some(M2Item::FrontPhtml(mod_name, template)) => { + let mut result = vec![]; + let mod_path = index.magento_modules.get(&mod_name); + if let Some(path) = mod_path { + let templ_path = path + .join("view") + .join("frontend") + .join("templates") + .join(&template); + if templ_path.is_file() { + result.push(Location { + uri: Url::from_file_path(templ_path).expect("Should be valid Url"), + range: Range::default(), + }); + } + }; - Some(result) - } - Some(M2Item::Class(class)) => { - let phpclass = get_php_class_from_class_name(index, &class)?; - index.php_classes.insert(class.clone(), phpclass.clone()); - Some(vec![Location { - uri: phpclass.uri.clone(), - range: phpclass.range, - }]) - } - Some(M2Item::Method(class, method)) => { - let phpclass = get_php_class_from_class_name(index, &class)?; - index.php_classes.insert(class.clone(), phpclass.clone()); - - 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, &class)?; - index.php_classes.insert(class.clone(), phpclass.clone()); - - Some(vec![Location { - uri: phpclass.uri.clone(), - range: phpclass - .constants - .get(&constant) - .map_or(phpclass.range, |method| method.range), - }]) - } - None => None, + for theme_path in index.magento_front_themes.values() { + let templ_path = theme_path.join(&mod_name).join("templates").join(&template); + if templ_path.is_file() { + result.push(Location { + uri: Url::from_file_path(templ_path).expect("Should be valid url"), + range: Range::default(), + }); + } + } + + Some(result) + } + Some(M2Item::Class(class)) => { + let phpclass = get_php_class_from_class_name(index, &class)?; + index.php_classes.insert(class.clone(), phpclass.clone()); + Some(vec![Location { + uri: phpclass.uri.clone(), + range: phpclass.range, + }]) + } + Some(M2Item::Method(class, method)) => { + let phpclass = get_php_class_from_class_name(index, &class)?; + index.php_classes.insert(class.clone(), phpclass.clone()); + + 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, &class)?; + index.php_classes.insert(class.clone(), phpclass.clone()); + + Some(vec![Location { + uri: phpclass.uri.clone(), + range: phpclass + .constants + .get(&constant) + .map_or(phpclass.range, |method| method.range), + }]) + } + None => None, + }, + _ => None, } } diff --git a/src/php.rs b/src/php.rs index a025f7b..ca3dd02 100644 --- a/src/php.rs +++ b/src/php.rs @@ -1,16 +1,18 @@ -use crate::{ - ts::{get_node_text, get_range_from_node}, - Indexer, -}; -use convert_case::{Case, Casing}; -use glob::glob; -use lsp_types::{Position, Range, Url}; use std::{ collections::HashMap, path::{Path, PathBuf}, }; + +use convert_case::{Case, Casing}; +use glob::glob; +use lsp_types::{Position, Range, Url}; use tree_sitter::{Node, Query, QueryCursor}; +use crate::{ + ts::{get_node_text, get_range_from_node}, + Indexer, +}; + #[derive(Debug, Clone)] pub struct Callable { pub class: String,