diff --git a/README.md b/README.md index 0697d23..5ba998f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ The missing language server for Drupal. - DataType - FormElement - RenderElement +### Code actions +- Add translation placeholders to `t()` functions. ## Installation @@ -73,4 +75,3 @@ The missing language server for Drupal. ### Code actions - [ ] Generate __construct doc block. -- [ ] Generate t function placeholder array. diff --git a/src/parser/php.rs b/src/parser/php.rs index 2ca31df..1871104 100644 --- a/src/parser/php.rs +++ b/src/parser/php.rs @@ -4,8 +4,7 @@ use std::collections::HashMap; use tree_sitter::{Node, Point}; use super::tokens::{ - ClassAttribute, DrupalHook, DrupalPlugin, DrupalPluginReference, DrupalPluginType, PhpClass, - PhpClassName, PhpMethod, Token, TokenData, + ClassAttribute, DrupalHook, DrupalPlugin, DrupalPluginReference, DrupalPluginType, DrupalTranslationString, PhpClass, PhpClassName, PhpMethod, Token, TokenData }; use super::{get_closest_parent_by_kind, get_node_at_position, get_tree, position_to_point}; @@ -73,7 +72,7 @@ impl PhpParser { match node.kind() { "class_declaration" => self.parse_class_declaration(node), "method_declaration" => self.parse_method_declaration(node), - "scoped_call_expression" | "member_call_expression" => { + "scoped_call_expression" | "member_call_expression" | "function_call_expression" => { self.parse_call_expression(node, point) } "function_definition" => self.parse_function_definition(node), @@ -122,7 +121,11 @@ 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_node = match node.kind() { + "function_call_expression" => node.child_by_field_name("function"), + _ => node.child_by_field_name("name"), + }?; + let name = self.get_node_text(&name_node); if node.kind() == "member_call_expression" { @@ -228,6 +231,14 @@ impl PhpParser { }), node.range(), )); + } else if name == "t" { + return Some(Token::new( + TokenData::DrupalTranslationString(DrupalTranslationString { + string: self.get_node_text(&string_content).to_string(), + placeholders: None, + }), + node.range(), + )); } None diff --git a/src/parser/tokens.rs b/src/parser/tokens.rs index 20ed81c..dbac27a 100644 --- a/src/parser/tokens.rs +++ b/src/parser/tokens.rs @@ -31,6 +31,7 @@ pub enum TokenData { DrupalPermissionDefinition(DrupalPermission), DrupalPermissionReference(String), DrupalPluginReference(DrupalPluginReference), + DrupalTranslationString(DrupalTranslationString), } #[derive(Debug, PartialEq, Clone)] @@ -195,6 +196,12 @@ pub struct DrupalPluginReference { pub plugin_id: String, } +#[derive(Debug)] +pub struct DrupalTranslationString { + pub string: String, + pub placeholders: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/server/handle_request.rs b/src/server/handle_request.rs index 3b9b7b9..993531e 100644 --- a/src/server/handle_request.rs +++ b/src/server/handle_request.rs @@ -1,6 +1,7 @@ use lsp_server::{ErrorCode, Request, RequestId, Response, ResponseError}; use super::handlers::completion::handle_text_document_completion; +use super::handlers::code_action::handle_text_document_code_action; use super::handlers::definition::handle_text_document_definition; use super::handlers::hover::handle_text_document_hover; @@ -10,6 +11,7 @@ pub fn handle_request(request: Request) -> Response { let request_id = request.id.clone(); let response = match request.method.as_str() { "textDocument/hover" => handle_text_document_hover(request), + "textDocument/codeAction" => handle_text_document_code_action(request), "textDocument/definition" => handle_text_document_definition(request), "textDocument/completion" => handle_text_document_completion(request), "shutdown" => None, diff --git a/src/server/handlers/code_action/mod.rs b/src/server/handlers/code_action/mod.rs new file mode 100644 index 0000000..fc916f7 --- /dev/null +++ b/src/server/handlers/code_action/mod.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use lsp_server::{ErrorCode, Request, Response}; +use lsp_types::{ + CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Uri, WorkspaceEdit, +}; +use regex::Regex; + +use crate::{ + document_store::DOCUMENT_STORE, + parser::tokens::{Token, TokenData}, + server::handle_request::get_response_error, +}; + +pub fn handle_text_document_code_action(request: Request) -> Option { + let params = match serde_json::from_value::(request.params) { + Err(err) => { + return Some(get_response_error( + request.id, + ErrorCode::InvalidParams, + format!("Could not parse code action params: {:?}", err), + )); + } + Ok(value) => value, + }; + + let mut token: Option = None; + if let Some(document) = DOCUMENT_STORE + .lock() + .unwrap() + .get_document(¶ms.text_document.uri.to_string()) + { + token = document.get_token_under_cursor(params.range.start); + } + + let mut code_actions_result: Vec = vec![]; + if let Some(token) = token { + if let TokenData::DrupalTranslationString(token_data) = &token.data { + let re = Regex::new(r#"(?[@%:]\w*)"#).unwrap(); + let arguments_string: String = format!( + ", [{}]", + re.captures_iter(&token_data.string) + .map(|capture| capture.name("placeholder")) + .filter_map(|str| Some(format!("'{}' => ''", str?.as_str()))) + .collect::>() + .join(", ") + ); + + let mut text_edits: HashMap> = HashMap::new(); + text_edits.insert( + params.text_document.uri, + vec![TextEdit { + range: Range { + start: Position { + line: token.range.end_point.row as u32, + character: token.range.end_point.column as u32 - 1, + }, + end: Position { + line: token.range.end_point.row as u32, + character: token.range.end_point.column as u32 - 1, + }, + }, + new_text: arguments_string, + }], + ); + + code_actions_result.push(CodeAction { + title: String::from("Add translations placeholders"), + kind: Some(CodeActionKind::REFACTOR_INLINE), + diagnostics: None, + edit: Some(WorkspaceEdit { + changes: Some(text_edits), + document_changes: None, + change_annotations: None, + }), + command: None, + is_preferred: Some(true), + disabled: None, + data: None, + }); + } + } + + match serde_json::to_value(code_actions_result) { + Ok(result) => Some(Response { + id: request.id, + result: Some(result), + error: None, + }), + Err(error) => Some(get_response_error( + request.id, + ErrorCode::InternalError, + format!("No code actions found: {:?}", error), + )), + } +} diff --git a/src/server/handlers/mod.rs b/src/server/handlers/mod.rs index 90e955b..2c316b6 100644 --- a/src/server/handlers/mod.rs +++ b/src/server/handlers/mod.rs @@ -1,3 +1,4 @@ pub mod completion; +pub mod code_action; pub mod definition; pub mod hover; diff --git a/src/server/mod.rs b/src/server/mod.rs index b2a6bf2..06beabc 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -50,6 +50,7 @@ pub async fn start_lsp(config: DrupalLspConfig) -> Result<()> { // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). let server_capabilities = serde_json::to_value(&ServerCapabilities { + code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)), text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), hover_provider: Some(HoverProviderCapability::Simple(true)), definition_provider: Some(lsp_types::OneOf::Left(true)),