diff --git a/src/document_store/mod.rs b/src/document_store/mod.rs index 2f5a11d..020d7d1 100644 --- a/src/document_store/mod.rs +++ b/src/document_store/mod.rs @@ -11,7 +11,9 @@ use lsp_types::TextDocumentContentChangeEvent; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use url::Url; -use crate::parser::tokens::{PhpClassName, PhpMethod, Token, TokenData}; +use crate::parser::tokens::{ + ClassAttribute, DrupalPluginReference, PhpClassName, PhpMethod, Token, TokenData, +}; use self::document::{Document, FileType}; @@ -37,7 +39,7 @@ pub fn initialize_document_store(root_dir: String) { override_builder .add("!**/core/lib/**/*Interface.php") .unwrap(); - override_builder.add("!**/Plugin/**/*.php").unwrap(); + override_builder.add("!**/tests/**/*.php").unwrap(); override_builder.add("!vendor").unwrap(); override_builder.add("!node_modules").unwrap(); override_builder.add("!libraries").unwrap(); @@ -195,7 +197,7 @@ impl DocumentStore { } pub fn get_method_definition(&self, method: &PhpMethod) -> Option<(&Document, &Token)> { - if let Some((document, token)) = self.get_class_definition(&method.class_name) { + if let Some((document, token)) = self.get_class_definition(&method.get_class(self)?) { if let TokenData::PhpClassDefinition(class) = &token.data { let token = class.methods.get(&method.name)?; return Some((document, token)); @@ -222,7 +224,6 @@ impl DocumentStore { pub fn get_permission_definition(&self, permission_name: &str) -> Option<(&Document, &Token)> { let files = self.get_documents_by_file_type(FileType::Yaml); - log::info!("{}", permission_name); files.iter().find_map(|&document| { Some(( @@ -237,6 +238,28 @@ impl DocumentStore { }) } + pub fn get_plugin_definition( + &self, + plugin_reference: &DrupalPluginReference, + ) -> Option<(&Document, &Token)> { + let files = self.get_documents_by_file_type(FileType::Php); + + files.iter().find_map(|&document| { + Some(( + document, + document.tokens.iter().find(|token| { + if let TokenData::PhpClassDefinition(class) = &token.data { + if let Some(ClassAttribute::Plugin(plugin)) = &class.attribute { + return plugin.plugin_type == plugin_reference.plugin_type + && plugin.plugin_id == plugin_reference.plugin_id; + } + } + false + })?, + )) + }) + } + fn get_documents_by_file_type(&self, file_type: FileType) -> Vec<&Document> { self.documents .values() diff --git a/src/documentation/mod.rs b/src/documentation/mod.rs index ed1668f..80fa899 100644 --- a/src/documentation/mod.rs +++ b/src/documentation/mod.rs @@ -83,7 +83,7 @@ pub fn get_documentation_for_token(token: &Token) -> Option { } TokenData::PhpMethodReference(method) => Some(format!( "PHP Method reference\nclass: {}\nmethod: {}", - method.class_name, method.name + method.class_name.clone()?, method.name )), TokenData::DrupalRouteReference(route_name) => { let store = DOCUMENT_STORE.lock().unwrap(); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index eec1b54..67a8602 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,8 +1,9 @@ -use tree_sitter::Node; - -pub mod yaml; pub mod php; pub mod tokens; +pub mod yaml; + +use lsp_types::Position; +use tree_sitter::{Language, Node, Parser, Point, Tree}; pub fn get_closest_parent_by_kind<'a>(node: &'a Node, kind: &'a str) -> Option> { let mut parent = node.parent(); @@ -11,3 +12,18 @@ pub fn get_closest_parent_by_kind<'a>(node: &'a Node, kind: &'a str) -> Option Option { + let mut parser = Parser::new(); + parser.set_language(language).ok()?; + parser.parse(source.as_bytes(), None) +} + +pub fn get_node_at_position(tree: &Tree, position: Position) -> Option { + let start = position_to_point(position); + tree.root_node().descendant_for_point_range(start, start) +} + +pub fn position_to_point(position: Position) -> Point { + Point::new(position.line as usize, position.character as usize) +} diff --git a/src/parser/php.rs b/src/parser/php.rs index b1e82f4..a1df873 100644 --- a/src/parser/php.rs +++ b/src/parser/php.rs @@ -1,17 +1,18 @@ -use std::collections::HashMap; - use lsp_types::Position; -use tree_sitter::{Node, Parser, Point, Tree}; +use regex::Regex; +use std::collections::HashMap; +use tree_sitter::{Node, Point}; -use super::get_closest_parent_by_kind; -use super::tokens::{DrupalHook, PhpClass, PhpClassName, PhpMethod, Token, TokenData}; +use super::tokens::{ + ClassAttribute, DrupalHook, DrupalPlugin, DrupalPluginReference, DrupalPluginType, PhpClass, + PhpClassName, PhpMethod, Token, TokenData, +}; +use super::{get_closest_parent_by_kind, get_node_at_position, get_tree, position_to_point}; pub struct PhpParser { source: String, } -// TODO: A lot of code has been copied from the yaml parser. -// How can we DRY this up? impl PhpParser { pub fn new(source: &str) -> Self { Self { @@ -19,23 +20,15 @@ impl PhpParser { } } - pub fn get_tree(&self) -> Option { - let mut parser = Parser::new(); - parser - .set_language(&tree_sitter_php::LANGUAGE_PHP.into()) - .ok()?; - parser.parse(self.source.as_bytes(), None) - } - pub fn get_tokens(&self) -> Vec { - let tree = self.get_tree(); + let tree = get_tree(&self.source, &tree_sitter_php::LANGUAGE_PHP.into()); self.parse_nodes(vec![tree.unwrap().root_node()]) } pub fn get_token_at_position(&self, position: Position) -> Option { - let tree = self.get_tree()?; - let mut node = self.get_node_at_position(&tree, position)?; - let point = self.position_to_point(position); + let tree = get_tree(&self.source, &tree_sitter_php::LANGUAGE_PHP.into())?; + let mut node = get_node_at_position(&tree, position)?; + let point = position_to_point(position); // Return the first "parseable" token in the parent chain. let mut parsed_node: Option; @@ -49,15 +42,6 @@ impl PhpParser { parsed_node } - fn get_node_at_position<'a>(&self, tree: &'a Tree, position: Position) -> Option> { - let start = self.position_to_point(position); - tree.root_node().descendant_for_point_range(start, start) - } - - fn position_to_point(&self, position: Position) -> Point { - Point::new(position.line as usize, position.character as usize) - } - fn parse_nodes(&self, nodes: Vec) -> Vec { let mut tokens: Vec = vec![]; @@ -138,13 +122,31 @@ impl PhpParser { fn parse_call_expression(&self, node: Node, point: Option) -> Option { let string_content = node.descendant_for_point_range(point?, point?)?; + let name_node = node.child_by_field_name("name")?; + let name = self.get_node_text(&name_node); + + if node.kind() == "member_call_expression" { + let object_node = node.child_by_field_name("object")?; + if self.get_node_text(&object_node).contains("Drupal::service") { + let arguments = object_node.child_by_field_name("arguments")?; + let service_name = self + .get_node_text(&arguments) + .trim_matches(|c| c == '\'' || c == '(' || c == ')'); + return Some(Token::new( + TokenData::PhpMethodReference(PhpMethod { + name: name.to_string(), + class_name: None, + service_name: Some(service_name.to_string()), + }), + node.range(), + )); + } + } + if string_content.kind() != "string_content" { return None; } - let name_node = node.child_by_field_name("name")?; - let name = self.get_node_text(&name_node); - if name == "fromRoute" || name == "createFromRoute" || name == "setRedirect" { return Some(Token::new( TokenData::DrupalRouteReference(self.get_node_text(&string_content).to_string()), @@ -165,16 +167,67 @@ impl PhpParser { } // TODO: This is a quite primitive way to detect ContainerInterface::get. // Can we somehow get the interface of a given variable? - if name == "get" { + else if name == "get" { let object_node = node.child_by_field_name("object")?; - if self.get_node_text(&object_node) == "$container" { + let object = self.get_node_text(&object_node); + if object == "$container" { return Some(Token::new( TokenData::DrupalServiceReference( self.get_node_text(&string_content).to_string(), ), node.range(), )); + } else if object.contains("queueFactory") { + return Some(Token::new( + TokenData::DrupalPluginReference(DrupalPluginReference { + plugin_type: DrupalPluginType::QueueWorker, + plugin_id: self.get_node_text(&string_content).to_string(), + }), + node.range(), + )); + } + } else if name == "getStorage" { + let object_node = node.child_by_field_name("object")?; + let object = self.get_node_text(&object_node); + if object.contains("entityTypeManager") { + return Some(Token::new( + TokenData::DrupalPluginReference(DrupalPluginReference { + plugin_type: DrupalPluginType::EntityType, + plugin_id: self.get_node_text(&string_content).to_string(), + }), + node.range(), + )); + } + } else if name == "create" { + let scope_node = node.child_by_field_name("scope")?; + if self + .get_node_text(&scope_node) + .contains("BaseFieldDefinition") + { + return Some(Token::new( + TokenData::DrupalPluginReference(DrupalPluginReference { + plugin_type: DrupalPluginType::FieldType, + plugin_id: self.get_node_text(&string_content).to_string(), + }), + node.range(), + )); + } else if self.get_node_text(&scope_node).contains("DataDefinition") { + return Some(Token::new( + TokenData::DrupalPluginReference(DrupalPluginReference { + plugin_type: DrupalPluginType::DataType, + plugin_id: self.get_node_text(&string_content).to_string(), + }), + node.range(), + )); } + } else if name == "queue" { + return Some(Token::new( + TokenData::DrupalPluginReference(DrupalPluginReference { + plugin_type: DrupalPluginType::QueueWorker, + plugin_id: self.get_node_text(&string_content).to_string(), + }), + node.range(), + )); } None @@ -193,14 +246,48 @@ impl PhpParser { }); } - let token = Token::new( + let mut class_attribute = None; + if let Some(attributes_node) = node.child_by_field_name("attributes") { + let attribute_group = attributes_node.child(0)?; + class_attribute = self.parse_class_attribute(attribute_group.named_child(0)?); + } else if let Some(comment_node) = node.prev_named_sibling() { + if comment_node.kind() == "comment" { + let text = self.get_node_text(&comment_node); + + let re = Regex::new(r#"\*\s*@(?.+)\("#).unwrap(); + let mut plugin_type: Option = None; + if let Some(captures) = re.captures(text) { + if let Some(str) = captures.name("type") { + plugin_type = DrupalPluginType::try_from(str.as_str()).ok(); + } + } + + let re = Regex::new(r#"id\s*=\s*"(?[^"]+)""#).unwrap(); + let mut plugin_id: Option = None; + if let Some(captures) = re.captures(text) { + if let Some(str) = captures.name("id") { + plugin_id = Some(str.as_str().to_string()); + } + } + + if let (Some(plugin_type), Some(plugin_id)) = (plugin_type, plugin_id) { + class_attribute = Some(ClassAttribute::Plugin(DrupalPlugin { + plugin_type, + plugin_id, + usage_example: self.extract_usage_example_from_comment(&comment_node), + })); + }; + } + } + + Some(Token::new( TokenData::PhpClassDefinition(PhpClass { name: self.get_class_name_from_node(node)?, + attribute: class_attribute, methods, }), node.range(), - ); - Some(token) + )) } fn parse_method_declaration(&self, node: Node) -> Option { @@ -214,12 +301,54 @@ impl PhpParser { Some(Token::new( TokenData::PhpMethodDefinition(PhpMethod { name: self.get_node_text(&name_node).to_string(), - class_name: self.get_class_name_from_node(class_node)?, + class_name: self.get_class_name_from_node(class_node), + service_name: None, }), node.range(), )) } + fn parse_class_attribute(&self, node: Node) -> Option { + if node.kind() != "attribute" { + return None; + } + + let mut plugin_id = String::default(); + + // TODO: Look into improving this if we want to extract more than plugin id. + let parameters_node = node.child_by_field_name("parameters")?; + for argument in parameters_node.named_children(&mut parameters_node.walk()) { + // In the case of f.e `#[FormElement('date')]` there is no `id` field. + if self.get_node_text(&argument).starts_with("'") + && self.get_node_text(&argument).ends_with("'") + { + plugin_id = self + .get_node_text(&argument) + .trim_matches(|c| c == '"' || c == '\'') + .to_string(); + break; + } + let argument_name = argument.child_by_field_name("name")?; + if self.get_node_text(&argument_name) == "id" { + plugin_id = self + .get_node_text(&argument.named_child(1)?) + .trim_matches(|c| c == '"' || c == '\'') + .to_string() + } + } + + match DrupalPluginType::try_from(self.get_node_text(&node.child(0)?)) { + Ok(plugin_type) => Some(ClassAttribute::Plugin(DrupalPlugin { + plugin_id, + plugin_type, + usage_example: self.extract_usage_example_from_comment( + &node.parent()?.parent()?.parent()?.prev_named_sibling()?, + ), + })), + Err(_) => None, + } + } + fn get_class_name_from_node(&self, node: Node) -> Option { if node.kind() != "class_declaration" { return None; @@ -245,4 +374,35 @@ impl PhpParser { fn get_node_text(&self, node: &Node) -> &str { node.utf8_text(self.source.as_bytes()).unwrap_or("") } + + /// Helper function to extract usage example from the preceding comment + fn extract_usage_example_from_comment(&self, comment_node: &Node) -> Option { + if comment_node.kind() != "comment" { + return None; + } + + let comment_text = self.get_node_text(comment_node); + let start_tag = "@code"; + let end_tag = "@endcode"; + + if let Some(start_index) = comment_text.find(start_tag) { + if let Some(end_index) = comment_text.find(end_tag) { + if end_index > start_index { + let code_start = start_index + start_tag.len(); + let example = comment_text[code_start..end_index].trim(); + let cleaned_example = example + .lines() + .map(|line| line.trim_start().strip_prefix("* ").unwrap_or(line)) + .collect::>(); + + let result = &cleaned_example[..cleaned_example.len() - 1] + .join("\n") + .trim() + .to_string(); + return Some(result.to_string()); + } + } + } + None + } } diff --git a/src/parser/tokens.rs b/src/parser/tokens.rs index 2c84a89..54977ab 100644 --- a/src/parser/tokens.rs +++ b/src/parser/tokens.rs @@ -1,7 +1,9 @@ use regex::Regex; -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; use tree_sitter::Range; +use crate::document_store::DocumentStore; + #[derive(Debug)] pub struct Token { pub range: Range, @@ -28,17 +30,18 @@ pub enum TokenData { DrupalHookDefinition(DrupalHook), DrupalPermissionDefinition(DrupalPermission), DrupalPermissionReference(String), + DrupalPluginReference(DrupalPluginReference), } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct PhpClassName { value: String, } -impl std::fmt::Display for PhpClassName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) - } +impl fmt::Display for PhpClassName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } } impl From<&str> for PhpClassName { @@ -51,16 +54,38 @@ impl From<&str> for PhpClassName { } } +#[derive(Debug)] +pub enum ClassAttribute { + Plugin(DrupalPlugin), +} + #[derive(Debug)] pub struct PhpClass { pub name: PhpClassName, + pub attribute: Option, pub methods: HashMap>, } #[derive(Debug)] pub struct PhpMethod { pub name: String, - pub class_name: PhpClassName, + pub class_name: Option, + pub service_name: Option, +} + +impl PhpMethod { + pub fn get_class(&self, store: &DocumentStore) -> Option { + if let Some(class_name) = &self.class_name { + return Some(class_name.clone()); + } else if let Some(service_name) = &self.service_name { + if let Some((_, token)) = store.get_service_definition(service_name) { + if let TokenData::DrupalServiceDefinition(service) = &token.data { + return Some(service.class.clone()); + } + } + } + None + } } impl TryFrom<&str> for PhpMethod { @@ -70,7 +95,8 @@ impl TryFrom<&str> for PhpMethod { if let Some((class, method)) = value.trim_matches(['\'', '\\']).split_once("::") { return Ok(Self { name: method.to_string(), - class_name: PhpClassName::from(class), + class_name: Some(PhpClassName::from(class)), + service_name: None, }); } @@ -124,6 +150,51 @@ pub struct DrupalPermission { pub title: String, } +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum DrupalPluginType { + EntityType, + QueueWorker, + FieldType, + DataType, + FormElement, + RenderElement, +} + +impl TryFrom<&str> for DrupalPluginType { + type Error = &'static str; + + fn try_from(value: &str) -> Result { + match value { + "ContentEntityType" | "ConfigEntityType" => Ok(DrupalPluginType::EntityType), + "QueueWorker" => Ok(DrupalPluginType::QueueWorker), + "FieldType" => Ok(DrupalPluginType::FieldType), + "DataType" => Ok(DrupalPluginType::DataType), + "FormElement" => Ok(DrupalPluginType::FormElement), + "RenderElement" => Ok(DrupalPluginType::RenderElement), + _ => Err("Unable to convert string to DrupalPluginType"), + } + } +} + +impl fmt::Display for DrupalPluginType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug)] +pub struct DrupalPlugin { + pub plugin_type: DrupalPluginType, + pub plugin_id: String, + pub usage_example: Option, +} + +#[derive(Debug)] +pub struct DrupalPluginReference { + pub plugin_type: DrupalPluginType, + pub plugin_id: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parser/yaml.rs b/src/parser/yaml.rs index e3b4376..408960c 100644 --- a/src/parser/yaml.rs +++ b/src/parser/yaml.rs @@ -1,12 +1,12 @@ use lsp_types::Position; use std::collections::HashMap; use std::vec; -use tree_sitter::{Node, Parser, Point, Tree}; +use tree_sitter::{Node, Point}; -use super::tokens::{ +use super::{get_node_at_position, get_tree, position_to_point, tokens::{ DrupalPermission, DrupalRoute, DrupalRouteDefaults, DrupalService, PhpClassName, PhpMethod, Token, TokenData, -}; +}}; pub struct YamlParser { source: String, @@ -21,21 +21,15 @@ impl YamlParser { } } - pub fn get_tree(&self) -> Option { - let mut parser = Parser::new(); - parser.set_language(&tree_sitter_yaml::language()).ok()?; - parser.parse(self.source.as_bytes(), None) - } - pub fn get_tokens(&self) -> Vec { - let tree = self.get_tree(); + let tree = get_tree(&self.source, &tree_sitter_yaml::language()); self.parse_nodes(vec![tree.unwrap().root_node()]) } pub fn get_token_at_position(&self, position: Position) -> Option { - let tree = self.get_tree()?; - let mut node = self.get_node_at_position(&tree, position)?; - let point = self.position_to_point(position); + let tree = get_tree(&self.source, &tree_sitter_yaml::language())?; + let mut node = get_node_at_position(&tree, position)?; + let point = position_to_point(position); // Return the first "parseable" token in the parent chain. let mut parsed_node: Option; @@ -49,15 +43,6 @@ impl YamlParser { parsed_node } - fn get_node_at_position<'a>(&self, tree: &'a Tree, position: Position) -> Option> { - let start = self.position_to_point(position); - tree.root_node().descendant_for_point_range(start, start) - } - - fn position_to_point(&self, position: Position) -> Point { - Point::new(position.line as usize, position.character as usize) - } - fn parse_nodes(&self, nodes: Vec) -> Vec { let mut tokens: Vec = vec![]; diff --git a/src/server/handlers/completion/mod.rs b/src/server/handlers/completion/mod.rs index ea7af7f..92b0854 100644 --- a/src/server/handlers/completion/mod.rs +++ b/src/server/handlers/completion/mod.rs @@ -10,7 +10,7 @@ use regex::Regex; use crate::document_store::DOCUMENT_STORE; use crate::documentation::get_documentation_for_token; -use crate::parser::tokens::{Token, TokenData}; +use crate::parser::tokens::{ClassAttribute, DrupalPluginType, Token, TokenData}; use crate::server::handle_request::get_response_error; pub fn handle_text_document_completion(request: Request) -> Option { @@ -44,8 +44,9 @@ pub fn handle_text_document_completion(request: Request) -> Option { token = document.get_token_under_cursor(position); } - let mut completion_items: Vec = get_global_snippets(); + let (file_name, extension) = uri.split('/').last()?.split_once('.')?; + let mut completion_items: Vec = get_global_snippets(); if let Some(token) = token { if let TokenData::DrupalRouteReference(_) = token.data { let re = Regex::new(r"(?.*fromRoute\(')(?[^']*)'(?, \[.*\])?"); @@ -130,6 +131,10 @@ pub fn handle_text_document_completion(request: Request) -> Option { } completion_items.push(CompletionItem { label: route.name.clone(), + label_details: Some(CompletionItemLabelDetails { + description: Some("Route".to_string()), + detail: None, + }), kind: Some(CompletionItemKind::REFERENCE), documentation, text_edit, @@ -155,6 +160,10 @@ pub fn handle_text_document_completion(request: Request) -> Option { } completion_items.push(CompletionItem { label: service.name.clone(), + label_details: Some(CompletionItemLabelDetails { + description: Some("Service".to_string()), + detail: None, + }), kind: Some(CompletionItemKind::REFERENCE), documentation, deprecated: Some(false), @@ -163,6 +172,26 @@ pub fn handle_text_document_completion(request: Request) -> Option { } }) }); + } else if let TokenData::PhpMethodReference(method) = token.data { + let store = DOCUMENT_STORE.lock().unwrap(); + // TODO: Don't suggest private/protected methods. + if let Some((_, class_token)) = store.get_class_definition(&method.get_class(&store)?) { + if let TokenData::PhpClassDefinition(class) = &class_token.data { + class.methods.keys().for_each(|method_name| { + completion_items.push(CompletionItem { + label: method_name.clone(), + label_details: Some(CompletionItemLabelDetails { + description: Some("Method".to_string()), + detail: None, + }), + kind: Some(CompletionItemKind::REFERENCE), + documentation: None, + deprecated: Some(false), + ..CompletionItem::default() + }); + }); + } + } } else if let TokenData::DrupalPermissionReference(_) = token.data { DOCUMENT_STORE .lock() @@ -180,6 +209,10 @@ pub fn handle_text_document_completion(request: Request) -> Option { // label. completion_items.push(CompletionItem { label: permission.name.clone(), + label_details: Some(CompletionItemLabelDetails { + description: Some("Permission".to_string()), + detail: None, + }), kind: Some(CompletionItemKind::REFERENCE), documentation, deprecated: Some(false), @@ -188,11 +221,42 @@ pub fn handle_text_document_completion(request: Request) -> Option { } }) }); + } else if let TokenData::DrupalPluginReference(plugin_reference) = token.data { + DOCUMENT_STORE + .lock() + .unwrap() + .get_documents() + .values() + .for_each(|document| { + document.tokens.iter().for_each(|token| { + if let TokenData::PhpClassDefinition(class) = &token.data { + if let Some(ClassAttribute::Plugin(plugin)) = &class.attribute { + if plugin_reference.plugin_type == plugin.plugin_type { + let mut documentation = None; + if let Some(documentation_string) = + get_documentation_for_token(token) + { + documentation = + Some(Documentation::String(documentation_string)); + } + completion_items.push(CompletionItem { + label: plugin.plugin_id.clone(), + label_details: Some(CompletionItemLabelDetails { + description: Some(plugin.plugin_type.to_string()), + detail: None, + }), + kind: Some(CompletionItemKind::REFERENCE), + documentation, + deprecated: Some(false), + ..CompletionItem::default() + }); + } + } + } + }) + }); } - } - - let (file_name, extension) = uri.split('/').last()?.split_once('.')?; - if extension == "module" || extension == "theme" { + } else if extension == "module" || extension == "theme" { DOCUMENT_STORE .lock() .unwrap() @@ -262,9 +326,13 @@ pub fn handle_text_document_completion(request: Request) -> Option { } fn get_global_snippets() -> Vec { - let mut snippets = HashMap::new(); + let mut snippets: HashMap = HashMap::new(); + + let mut add_snippet = |key: &str, value: &str| { + snippets.insert(key.into(), value.into()); + }; - snippets.insert( + add_snippet( "batch", r#" \$storage = \\Drupal::entityTypeManager()->getStorage('$0'); @@ -285,26 +353,26 @@ if (\$sandbox['total'] > 0) { \$sandbox['#finished'] = (\$sandbox['total'] - count(\$sandbox['ids'])) / \$sandbox['total']; }"#, ); - snippets.insert( + add_snippet( "ihdoc", r#" /** * {@inheritdoc} */"#, ); - snippets.insert( + add_snippet( "ensure-instanceof", "if (!($1 instanceof $2)) {\n return$0;\n}", ); - snippets.insert( + add_snippet( "entity-storage", "\\$storage = \\$this->entityTypeManager->getStorage('$0');", ); - snippets.insert( + add_snippet( "entity-load", "\\$$1 = \\$this->entityTypeManager->getStorage('$1')->load($0);", ); - snippets.insert( + add_snippet( "entity-query", r#" \$ids = \$this->entityTypeManager->getStorage('$1')->getQuery() @@ -312,28 +380,28 @@ if (\$sandbox['total'] > 0) { $0 ->execute()"#, ); - snippets.insert("type", "'#type' => '$0',"); - snippets.insert("title", "'#title' => \\$this->t('$0'),"); - snippets.insert("description", "'#description' => \\$this->t('$0'),"); - snippets.insert("attributes", "'#attributes' => [$0],"); - snippets.insert( + add_snippet("type", "'#type' => '$0',"); + add_snippet("title", "'#title' => \\$this->t('$0'),"); + add_snippet("description", "'#description' => \\$this->t('$0'),"); + add_snippet("attributes", "'#attributes' => [$0],"); + add_snippet( "attributes-class", "'#attributes' => [\n 'class' => ['$0'],\n],", ); - snippets.insert("attributes-id", "'#attributes' => [\n 'id' => '$0',\n],"); - snippets.insert( + add_snippet("attributes-id", "'#attributes' => [\n 'id' => '$0',\n],"); + add_snippet( "type_html_tag", r#"'#type' => 'html_tag', '#tag' => '$1', '#value' => $0,"#, ); - snippets.insert( + add_snippet( "type_details", r#"'#type' => 'details', '#open' => TRUE, '#title' => \$this->t('$0'),"#, ); - snippets.insert( + add_snippet( "create", r#"/** * {@inheritdoc} @@ -344,7 +412,7 @@ public static function create(ContainerInterface \$container) { ); }"#, ); - snippets.insert( + add_snippet( "create-plugin", r#"/** * {@inheritdoc} @@ -359,12 +427,47 @@ public static function create(ContainerInterface \$container, array \$configurat }"#, ); + // Create pre-generated snippets. + DOCUMENT_STORE + .lock() + .unwrap() + .get_documents() + .values() + .flat_map(|document| document.tokens.iter()) + .filter_map(|token| match &token.data { + TokenData::PhpClassDefinition(class_def) => match &class_def.attribute { + Some(ClassAttribute::Plugin(plugin)) => Some(plugin), + _ => None, + }, + _ => None, + }) + .filter_map(|plugin| { + let snippet_key_prefix = match plugin.plugin_type { + DrupalPluginType::RenderElement => Some("render"), + DrupalPluginType::FormElement => Some("form"), + _ => None, + }; + + snippet_key_prefix.and_then(|prefix| { + plugin + .usage_example + .as_ref() + .map(|usage_example| (prefix, &plugin.plugin_id, usage_example)) + }) + }) + .for_each(|(snippet_key_prefix, plugin_id, usage_example)| { + snippets.insert( + format!("{}-{}", snippet_key_prefix, plugin_id), + usage_example.replace("$", "\\$"), + ); + }); + snippets .iter() .map(|(name, snippet)| CompletionItem { - label: name.to_string(), + label: name.clone(), kind: Some(CompletionItemKind::SNIPPET), - insert_text: Some(snippet.to_string()), + insert_text: Some(snippet.clone()), insert_text_format: Some(InsertTextFormat::SNIPPET), deprecated: Some(false), ..CompletionItem::default() diff --git a/src/server/handlers/definition/mod.rs b/src/server/handlers/definition/mod.rs index 47d4ec3..057f857 100644 --- a/src/server/handlers/definition/mod.rs +++ b/src/server/handlers/definition/mod.rs @@ -57,12 +57,11 @@ fn provide_definition_for_token(token: &Token) -> Option let (source_document, token) = match &token.data { TokenData::PhpClassReference(class) => store.get_class_definition(class), TokenData::PhpMethodReference(method) => store.get_method_definition(method), - TokenData::DrupalServiceReference(service_name) => { - store.get_service_definition(service_name) - } - TokenData::DrupalRouteReference(route_name) => store.get_route_definition(route_name), - TokenData::DrupalHookReference(hook_name) => store.get_hook_definition(hook_name), - TokenData::DrupalPermissionReference(permission_name) => store.get_permission_definition(permission_name), + TokenData::DrupalServiceReference(name) => store.get_service_definition(name), + TokenData::DrupalRouteReference(name) => store.get_route_definition(name), + TokenData::DrupalHookReference(name) => store.get_hook_definition(name), + TokenData::DrupalPermissionReference(name) => store.get_permission_definition(name), + TokenData::DrupalPluginReference(plugin_id) => store.get_plugin_definition(plugin_id), _ => None, }?;