diff --git a/src/php.rs b/src/php.rs index 09251bd..a025f7b 100644 --- a/src/php.rs +++ b/src/php.rs @@ -17,7 +17,7 @@ pub struct Callable { pub method: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum M2Item { Class(String), Method(String, String), diff --git a/src/xml.rs b/src/xml.rs index dd95b2b..1f55a7b 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -1,116 +1,208 @@ -use std::path::Path; - use crate::{ php::M2Item, ts::{get_node_text, node_at_position}, }; use lsp_types::{Position, Url}; +use std::{collections::HashMap, path::Path}; use tree_sitter::{Query, QueryCursor}; +#[derive(Debug, Clone)] +enum XmlPart { + Text, + Attribute(String), + None, +} + +#[derive(Debug, Clone)] +struct XmlTag { + name: String, + attributes: HashMap, + text: String, + hover_on: XmlPart, +} + +impl XmlTag { + fn new() -> Self { + XmlTag { + name: String::new(), + attributes: HashMap::new(), + text: String::new(), + hover_on: XmlPart::None, + } + } +} + pub fn get_item_from_position(uri: &Url, pos: Position) -> Option { - let path = uri.path(); + 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(&content, uri, pos) +} + +fn get_item_from_pos(content: &str, uri: &Url, pos: Position) -> Option { + let path = uri.to_file_path().expect("Should be valid file path"); + let path = path.to_str()?; + let tag = get_xml_tag_at_pos(content, pos)?; + + match tag.hover_on { + XmlPart::Attribute(ref attr_name) => match attr_name.as_str() { + "method" | "instance" | "class" => try_method_item_from_tag(&tag), + "template" => { + try_phtml_item_from_str(tag.attributes.get(attr_name)?, is_frontend_location(path)) + } + _ => try_any_item_from_str(tag.attributes.get(attr_name)?, is_frontend_location(path)), + }, + XmlPart::Text => { + let text = tag.text.trim_matches('\\'); + let empty = String::new(); + let xsi_type = tag.attributes.get("xsi:type").unwrap_or(&empty); + + match xsi_type.as_str() { + "object" => Some(get_class_item_from_str(text)), + "init_parameter" => try_const_item_from_str(text), + _ => try_any_item_from_str(text, is_frontend_location(path)), + } + } + XmlPart::None => None, + } +} +fn get_xml_tag_at_pos(content: &str, pos: Position) -> Option { let query_string = " - (attribute_value) @attr - (text) @text - - (self_closing_tag (tag_name) - (attribute (attribute_name ) @_attr2 (#eq? @_attr2 \"class\") - (quoted_attribute_value (attribute_value) @class)) - ) @callable - (self_closing_tag (tag_name) - (attribute (attribute_name) @_attr (#eq? @_attr \"method\") - (quoted_attribute_value (attribute_value) @method)) - ) @callable - (self_closing_tag (tag_name) @_name - (attribute (attribute_name ) @_attr2 (#eq? @_attr2 \"instance\") - (quoted_attribute_value (attribute_value) @class)) - ) @callable - (start_tag (tag_name) - (attribute (attribute_name ) @_attr2 (#eq? @_attr2 \"class\") - (quoted_attribute_value (attribute_value) @class)) - ) @callable - (start_tag (tag_name) - (attribute (attribute_name) @_attr (#eq? @_attr \"method\") - (quoted_attribute_value (attribute_value) @method)) - ) @callable - (start_tag (tag_name) @_name - (attribute (attribute_name ) @_attr2 (#eq? @_attr2 \"instance\") - (quoted_attribute_value (attribute_value) @class)) - ) @callable + (element + (start_tag + (tag_name) @tag_name + (attribute + (attribute_name) @attr_name + (quoted_attribute_value (attribute_value) @attr_val) + )? + ) @tag + (text)? @text + ) + (element + (self_closing_tag + (tag_name) @tag_name + (attribute + (attribute_name) @attr_name + (quoted_attribute_value (attribute_value) @attr_val) + ) + ) @tag + ) "; - let content = std::fs::read_to_string(path).expect("Should have been able to read the file"); - - let tree = tree_sitter_parsers::parse(&content, "html"); + let tree = tree_sitter_parsers::parse(content, "html"); let query = Query::new(tree.language(), query_string) .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()); - - let mut class_name: Option = None; - let mut method_name: Option = None; - - // FIXME its ugly as fuck, figure out better way to get this data - for m in matches { - let node = m.captures[0].node; - if node_at_position(node, pos) { - if node.kind() == "attribute_value" || node.kind() == "text" { - class_name = Some(get_node_text(node, &content)); - } else if node.kind() == "self_closing_tag" || node.kind() == "start_tag" { - let mut cursor = node.walk(); - for child in node.named_children(&mut cursor) { - if child.kind() == "attribute" { - let attr_name = child - .named_child(0) - .map_or_else(String::new, |attr| get_node_text(attr, &content)); - if attr_name == "class" || attr_name == "instance" { - class_name = Some(get_node_text( - child.named_child(1)?.named_child(0)?, - &content, - )); - } - if attr_name == "method" { - method_name = Some(get_node_text( - child.named_child(1)?.named_child(0)?, - &content, - )); - } - } + let captures = cursor.captures(&query, tree.root_node(), content.as_bytes()); + + let mut last_attribute_name = String::new(); + let mut last_tag_id: Option = None; + let mut tag = XmlTag::new(); + + for (m, i) in captures { + let first = m.captures[0].node; // always (self)opening tag + let last = m.captures[m.captures.len() - 1].node; + if !node_at_position(first, pos) && !node_at_position(last, pos) { + continue; + } + let id = m.captures[0].node.id(); // id of tag name + if last_tag_id.is_none() || last_tag_id != Some(id) { + last_tag_id = Some(id); + tag = XmlTag::new(); + } + let node = m.captures[i].node; + let hovered = node_at_position(node, pos); + match node.kind() { + "tag_name" => { + tag.name = get_node_text(node, content); + } + "attribute_name" => { + last_attribute_name = get_node_text(node, content); + } + "attribute_value" => { + tag.attributes + .insert(last_attribute_name.clone(), get_node_text(node, content)); + if hovered { + tag.hover_on = XmlPart::Attribute(last_attribute_name.clone()); + } + } + "text" => { + tag.text = get_node_text(node, content); + if hovered { + tag.hover_on = XmlPart::Text; } } + _ => (), } } - match (class_name, method_name) { - (Some(class), Some(method)) => Some(M2Item::Method(class, method)), - (Some(class), None) => { - if does_ext_eq(&class, "phtml") { - let mut parts = class.split("::"); - if is_frontend_location(path) { - Some(M2Item::FrontPhtml( - parts.next()?.to_string(), - parts.next()?.to_string(), - )) - } else { - Some(M2Item::AdminPhtml( - parts.next()?.to_string(), - parts.next()?.to_string(), - )) - } - } else if class.contains("::") { - let mut parts = class.split("::"); - Some(M2Item::Const( - parts.next()?.to_string(), - parts.next()?.to_string(), - )) - } else { - Some(M2Item::Class(class)) - } + match tag.hover_on { + XmlPart::None => None, + _ => Some(tag), + } +} + +fn try_any_item_from_str(text: &str, is_frontend: bool) -> Option { + if does_ext_eq(text, "phtml") { + try_phtml_item_from_str(text, is_frontend) + } else if text.contains("::") { + try_const_item_from_str(text) + } else { + Some(get_class_item_from_str(text)) + } +} + +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(), + )) + } else { + None + } +} + +fn get_class_item_from_str(text: &str) -> M2Item { + M2Item::Class(text.to_string()) +} + +fn try_phtml_item_from_str(text: &str, is_frontend: bool) -> Option { + if text.split("::").count() == 2 { + let mut parts = text.split("::"); + if is_frontend { + Some(M2Item::FrontPhtml( + parts.next()?.to_string(), + parts.next()?.to_string(), + )) + } else { + Some(M2Item::AdminPhtml( + parts.next()?.to_string(), + parts.next()?.to_string(), + )) } - _ => None, + } else { + None + } +} + +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(), + )) + } 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(), + )) + } else { + None } } @@ -126,3 +218,171 @@ fn does_ext_eq(path: &str, ext: &str) -> bool { .extension() .map_or(false, |e| e.eq_ignore_ascii_case(ext)) } + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + fn get_test_item(xml: &str, path: &str) -> Option { + 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(path)).unwrap(); + get_item_from_pos(&xml.replace('|', ""), &uri, pos) + } + + #[test] + fn test_get_item_from_pos_class_in_tag_text() { + let item = get_test_item(r#"|A\B\C"#, "/a/b/c"); + + assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + } + + #[test] + fn test_get_item_from_pos_template_in_tag_attribute() { + let item = get_test_item( + r#""#, + "/a/b/c", + ); + assert_eq!( + item, + Some(M2Item::AdminPhtml( + "Some_Module".to_string(), + "path/to/file.phtml".to_string() + )) + ); + } + + #[test] + fn test_get_item_from_pos_frontend_template_in_tag_attribute() { + let item = get_test_item( + r#""#, + "/a/view/frontend/c", + ); + assert_eq!( + item, + Some(M2Item::FrontPhtml( + "Some_Module".to_string(), + "path/to/file.phtml".to_string() + )) + ); + } + + #[test] + fn test_get_item_from_pos_method_in_job_tag_attribute() { + let item = get_test_item( + r#""#, + "/a/a/c", + ); + assert_eq!( + item, + Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + ); + } + + #[test] + fn test_get_item_from_pos_method_in_service_tag_attribute() { + let item = get_test_item( + r#""#, + "/a/a/c", + ); + assert_eq!( + item, + Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + ); + } + + #[test] + fn test_get_item_from_pos_class_in_service_tag_attribute() { + let item = get_test_item( + r#"xx"#, + "/a/a/c", + ); + assert_eq!( + item, + Some(M2Item::Method("A\\B\\C".to_string(), "metHod".to_string())) + ); + } + + #[test] + fn test_get_item_from_pos_attribute_in_tag_with_method() { + let item = get_test_item( + r#"xx"#, + "/a/a/c", + ); + assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + } + + #[test] + fn test_get_item_from_pos_class_in_text_in_tag() { + let item = get_test_item(r#"|A\B\C"#, "/a/a/c"); + assert_eq!(item, Some(M2Item::Class("A\\B\\C".to_string()))); + } + + #[test] + fn test_get_item_from_pos_const_in_text_in_tag() { + let item = get_test_item( + r#"\|A\B\C::CONST_ANT"#, + "/a/a/c", + ); + assert_eq!( + item, + Some(M2Item::Const( + "A\\B\\C".to_string(), + "CONST_ANT".to_string() + )) + ); + } + + #[test] + fn test_get_item_from_pos_template_in_text_in_tag() { + let item = get_test_item( + r#"Some_Module::fi|le.phtml"#, + "/a/a/c", + ); + assert_eq!( + item, + Some(M2Item::AdminPhtml( + "Some_Module".to_string(), + "file.phtml".to_string() + )) + ); + } + + #[test] + fn test_get_item_from_pos_method_attribute_in_tag() { + let item = get_test_item( + r#"xx"#, + "/a/a/c", + ); + assert_eq!(item, None) + } + + #[test] + fn test_should_get_most_inner_tag_from_nested() { + let item = get_test_item( + r#" + + + + Some\Cl|ass\Name + multiselect + select + \\A\\B\\C + + + + "#, + "/a/a/c", + ); + assert_eq!(item, Some(M2Item::Class("Some\\Class\\Name".to_string()))) + } +}