Skip to content

Commit 28a4ec0

Browse files
authored
Merge pull request #8 from jdrupal-dev/feature/generate-translation-placeholders
feat: Add code action to generate placeholders for translation strings
2 parents d5fb28a + 45457e7 commit 28a4ec0

7 files changed

Lines changed: 124 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ The missing language server for Drupal.
3434
- DataType
3535
- FormElement
3636
- RenderElement
37+
### Code actions
38+
- Add translation placeholders to `t()` functions.
3739

3840
## Installation
3941

@@ -73,4 +75,3 @@ The missing language server for Drupal.
7375

7476
### Code actions
7577
- [ ] Generate __construct doc block.
76-
- [ ] Generate t function placeholder array.

src/parser/php.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ use std::collections::HashMap;
44
use tree_sitter::{Node, Point};
55

66
use super::tokens::{
7-
ClassAttribute, DrupalHook, DrupalPlugin, DrupalPluginReference, DrupalPluginType, PhpClass,
8-
PhpClassName, PhpMethod, Token, TokenData,
7+
ClassAttribute, DrupalHook, DrupalPlugin, DrupalPluginReference, DrupalPluginType, DrupalTranslationString, PhpClass, PhpClassName, PhpMethod, Token, TokenData
98
};
109
use super::{get_closest_parent_by_kind, get_node_at_position, get_tree, position_to_point};
1110

@@ -73,7 +72,7 @@ impl PhpParser {
7372
match node.kind() {
7473
"class_declaration" => self.parse_class_declaration(node),
7574
"method_declaration" => self.parse_method_declaration(node),
76-
"scoped_call_expression" | "member_call_expression" => {
75+
"scoped_call_expression" | "member_call_expression" | "function_call_expression" => {
7776
self.parse_call_expression(node, point)
7877
}
7978
"function_definition" => self.parse_function_definition(node),
@@ -122,7 +121,11 @@ impl PhpParser {
122121

123122
fn parse_call_expression(&self, node: Node, point: Option<Point>) -> Option<Token> {
124123
let string_content = node.descendant_for_point_range(point?, point?)?;
125-
let name_node = node.child_by_field_name("name")?;
124+
let name_node = match node.kind() {
125+
"function_call_expression" => node.child_by_field_name("function"),
126+
_ => node.child_by_field_name("name"),
127+
}?;
128+
126129
let name = self.get_node_text(&name_node);
127130

128131
if node.kind() == "member_call_expression" {
@@ -228,6 +231,14 @@ impl PhpParser {
228231
}),
229232
node.range(),
230233
));
234+
} else if name == "t" {
235+
return Some(Token::new(
236+
TokenData::DrupalTranslationString(DrupalTranslationString {
237+
string: self.get_node_text(&string_content).to_string(),
238+
placeholders: None,
239+
}),
240+
node.range(),
241+
));
231242
}
232243

233244
None

src/parser/tokens.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub enum TokenData {
3131
DrupalPermissionDefinition(DrupalPermission),
3232
DrupalPermissionReference(String),
3333
DrupalPluginReference(DrupalPluginReference),
34+
DrupalTranslationString(DrupalTranslationString),
3435
}
3536

3637
#[derive(Debug, PartialEq, Clone)]
@@ -195,6 +196,12 @@ pub struct DrupalPluginReference {
195196
pub plugin_id: String,
196197
}
197198

199+
#[derive(Debug)]
200+
pub struct DrupalTranslationString {
201+
pub string: String,
202+
pub placeholders: Option<String>,
203+
}
204+
198205
#[cfg(test)]
199206
mod tests {
200207
use super::*;

src/server/handle_request.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use lsp_server::{ErrorCode, Request, RequestId, Response, ResponseError};
22

33
use super::handlers::completion::handle_text_document_completion;
4+
use super::handlers::code_action::handle_text_document_code_action;
45
use super::handlers::definition::handle_text_document_definition;
56
use super::handlers::hover::handle_text_document_hover;
67

@@ -10,6 +11,7 @@ pub fn handle_request(request: Request) -> Response {
1011
let request_id = request.id.clone();
1112
let response = match request.method.as_str() {
1213
"textDocument/hover" => handle_text_document_hover(request),
14+
"textDocument/codeAction" => handle_text_document_code_action(request),
1315
"textDocument/definition" => handle_text_document_definition(request),
1416
"textDocument/completion" => handle_text_document_completion(request),
1517
"shutdown" => None,
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use std::collections::HashMap;
2+
3+
use lsp_server::{ErrorCode, Request, Response};
4+
use lsp_types::{
5+
CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Uri, WorkspaceEdit,
6+
};
7+
use regex::Regex;
8+
9+
use crate::{
10+
document_store::DOCUMENT_STORE,
11+
parser::tokens::{Token, TokenData},
12+
server::handle_request::get_response_error,
13+
};
14+
15+
pub fn handle_text_document_code_action(request: Request) -> Option<Response> {
16+
let params = match serde_json::from_value::<CodeActionParams>(request.params) {
17+
Err(err) => {
18+
return Some(get_response_error(
19+
request.id,
20+
ErrorCode::InvalidParams,
21+
format!("Could not parse code action params: {:?}", err),
22+
));
23+
}
24+
Ok(value) => value,
25+
};
26+
27+
let mut token: Option<Token> = None;
28+
if let Some(document) = DOCUMENT_STORE
29+
.lock()
30+
.unwrap()
31+
.get_document(&params.text_document.uri.to_string())
32+
{
33+
token = document.get_token_under_cursor(params.range.start);
34+
}
35+
36+
let mut code_actions_result: Vec<CodeAction> = vec![];
37+
if let Some(token) = token {
38+
if let TokenData::DrupalTranslationString(token_data) = &token.data {
39+
let re = Regex::new(r#"(?<placeholder>[@%:]\w*)"#).unwrap();
40+
let arguments_string: String = format!(
41+
", [{}]",
42+
re.captures_iter(&token_data.string)
43+
.map(|capture| capture.name("placeholder"))
44+
.filter_map(|str| Some(format!("'{}' => ''", str?.as_str())))
45+
.collect::<Vec<String>>()
46+
.join(", ")
47+
);
48+
49+
let mut text_edits: HashMap<Uri, Vec<TextEdit>> = HashMap::new();
50+
text_edits.insert(
51+
params.text_document.uri,
52+
vec![TextEdit {
53+
range: Range {
54+
start: Position {
55+
line: token.range.end_point.row as u32,
56+
character: token.range.end_point.column as u32 - 1,
57+
},
58+
end: Position {
59+
line: token.range.end_point.row as u32,
60+
character: token.range.end_point.column as u32 - 1,
61+
},
62+
},
63+
new_text: arguments_string,
64+
}],
65+
);
66+
67+
code_actions_result.push(CodeAction {
68+
title: String::from("Add translations placeholders"),
69+
kind: Some(CodeActionKind::REFACTOR_INLINE),
70+
diagnostics: None,
71+
edit: Some(WorkspaceEdit {
72+
changes: Some(text_edits),
73+
document_changes: None,
74+
change_annotations: None,
75+
}),
76+
command: None,
77+
is_preferred: Some(true),
78+
disabled: None,
79+
data: None,
80+
});
81+
}
82+
}
83+
84+
match serde_json::to_value(code_actions_result) {
85+
Ok(result) => Some(Response {
86+
id: request.id,
87+
result: Some(result),
88+
error: None,
89+
}),
90+
Err(error) => Some(get_response_error(
91+
request.id,
92+
ErrorCode::InternalError,
93+
format!("No code actions found: {:?}", error),
94+
)),
95+
}
96+
}

src/server/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod completion;
2+
pub mod code_action;
23
pub mod definition;
34
pub mod hover;

src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub async fn start_lsp(config: DrupalLspConfig) -> Result<()> {
5050

5151
// Run the server and wait for the two threads to end (typically by trigger LSP Exit event).
5252
let server_capabilities = serde_json::to_value(&ServerCapabilities {
53+
code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)),
5354
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
5455
hover_provider: Some(HoverProviderCapability::Simple(true)),
5556
definition_provider: Some(lsp_types::OneOf::Left(true)),

0 commit comments

Comments
 (0)