Skip to content

Commit b0d49d6

Browse files
committed
feat: permission completion/hover/definition + route parameter completion
1 parent ba0f6c7 commit b0d49d6

File tree

10 files changed

+300
-18
lines changed

10 files changed

+300
-18
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,26 @@ WIP!
1010
- Route references
1111
- Route controller/form
1212
- Hook references
13+
- Permission references
1314
### Go to definition
1415
- Service references
1516
- Service class
1617
- Route references
1718
- Route controller/form
1819
- Hook references
20+
- Permission references
1921
### Completion
2022
- Services
2123
- Routes
2224
- Hook snippets
2325
- General snippets
26+
- Permissions
2427

2528
## Roadmap
2629
### Completion
27-
- [ ] Autocomplete permissions.
2830
- [ ] Autocomplete plugin IDs (eg. queue workers, blocks, fields, migrate source/process/destination).
31+
- [ ] Autocomplete #theme functions.
2932

33+
### Code actions
34+
- [ ] Generate __construct doc block.
35+
- [ ] Generate t function placeholder array.

src/document_store/document.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ impl Document {
4242
parser.get_tokens()
4343
}
4444
FileType::Yaml => {
45-
let parser = YamlParser::new(&self.content);
45+
let parser = YamlParser::new(&self.content, &self.uri);
4646
parser.get_tokens()
4747
}
4848
FileType::Unknown => {
@@ -61,11 +61,11 @@ impl Document {
6161
FileType::Php => {
6262
let parser = PhpParser::new(&self.content);
6363
return parser.get_token_at_position(position);
64-
},
64+
}
6565
FileType::Yaml => {
66-
let parser = YamlParser::new(&self.content);
66+
let parser = YamlParser::new(&self.content, &self.uri);
6767
return parser.get_token_at_position(position);
68-
},
68+
}
6969
_ => None,
7070
}
7171
}

src/document_store/mod.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ pub fn initialize_document_store(root_dir: String) {
2828
let mut override_builder = OverrideBuilder::new(&root_dir);
2929
override_builder.add("**/*.services.yml").unwrap();
3030
override_builder.add("**/*.routing.yml").unwrap();
31+
override_builder.add("**/*.permissions.yml").unwrap();
32+
override_builder.add("**/*.menu.yml").unwrap();
3133
override_builder.add("**/src/**/*.php").unwrap();
3234
override_builder.add("**/core/lib/**/*.php").unwrap();
3335
// For now we don't care about interfaces at all.
3436
override_builder.add("!**/src/**/*Interface.php").unwrap();
35-
override_builder.add("!**/core/lib/**/*Interface.php").unwrap();
37+
override_builder
38+
.add("!**/core/lib/**/*Interface.php")
39+
.unwrap();
3640
override_builder.add("!**/Plugin/**/*.php").unwrap();
3741
override_builder.add("!vendor").unwrap();
3842
override_builder.add("!node_modules").unwrap();
@@ -80,7 +84,7 @@ pub fn initialize_document_store(root_dir: String) {
8084
log::info!(
8185
"Parsed {} files in {} seconds",
8286
documents.len(),
83-
now.elapsed().unwrap().as_secs()
87+
now.elapsed().unwrap().as_secs_f64()
8488
);
8589

8690
DOCUMENT_STORE.lock().unwrap().add_documents(documents);
@@ -216,6 +220,23 @@ impl DocumentStore {
216220
})
217221
}
218222

223+
pub fn get_permission_definition(&self, permission_name: &str) -> Option<(&Document, &Token)> {
224+
let files = self.get_documents_by_file_type(FileType::Yaml);
225+
log::info!("{}", permission_name);
226+
227+
files.iter().find_map(|&document| {
228+
Some((
229+
document,
230+
document.tokens.iter().find(|token| {
231+
if let TokenData::DrupalPermissionDefinition(permission) = &token.data {
232+
return permission.name == permission_name;
233+
}
234+
return false;
235+
})?,
236+
))
237+
})
238+
}
239+
219240
fn get_documents_by_file_type(&self, file_type: FileType) -> Vec<&Document> {
220241
self.documents
221242
.values()

src/documentation/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ const HOOK_DEFINITION: &str = r#"
6060
```
6161
"#;
6262

63+
const PERMISSION_REFERENCE: &str = r#"
64+
# Permission reference: @name
65+
66+
*Implementation:*
67+
```yaml
68+
@definition
69+
```
70+
71+
@see [@uri](@uri)
72+
"#;
73+
74+
const PERMISSION_DEFINITION: &str = r#"
75+
# Permission: @name
76+
Title: @title
77+
"#;
78+
6379
pub fn get_documentation_for_token(token: &Token) -> Option<String> {
6480
match &token.data {
6581
TokenData::PhpClassReference(class) => {
@@ -137,6 +153,28 @@ pub fn get_documentation_for_token(token: &Token) -> Option<String> {
137153
&hook.parameters.clone().unwrap_or(String::default()),
138154
))
139155
}
156+
TokenData::DrupalPermissionReference(permission_name) => {
157+
let store = DOCUMENT_STORE.lock().unwrap();
158+
159+
let (source_document, token) = store.get_permission_definition(permission_name)?;
160+
if let TokenData::DrupalPermissionDefinition(permission) = &token.data {
161+
let definition =
162+
&source_document.content[token.range.start_byte..token.range.end_byte];
163+
164+
return Some(
165+
PERMISSION_REFERENCE
166+
.replace("@name", &permission.name)
167+
.replace("@uri", source_document.get_uri()?.as_str())
168+
.replace("@definition", definition),
169+
);
170+
}
171+
None
172+
}
173+
TokenData::DrupalPermissionDefinition(permission) => Some(
174+
PERMISSION_DEFINITION
175+
.replace("@name", &permission.name)
176+
.replace("@title", &permission.title),
177+
),
140178
_ => None,
141179
}
142180
}

src/parser/php.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,23 @@ impl PhpParser {
145145
let name_node = node.child_by_field_name("name")?;
146146
let name = self.get_node_text(&name_node);
147147

148-
if name == "fromRoute" || name == "createFromRoute" {
148+
if name == "fromRoute" || name == "createFromRoute" || name == "setRedirect" {
149149
return Some(Token::new(
150150
TokenData::DrupalRouteReference(self.get_node_text(&string_content).to_string()),
151151
node.range(),
152152
));
153-
}
154-
if name == "service" {
153+
} else if name == "service" {
155154
return Some(Token::new(
156155
TokenData::DrupalServiceReference(self.get_node_text(&string_content).to_string()),
157156
node.range(),
158157
));
158+
} else if name == "hasPermission" {
159+
return Some(Token::new(
160+
TokenData::DrupalPermissionReference(
161+
self.get_node_text(&string_content).to_string(),
162+
),
163+
node.range(),
164+
));
159165
}
160166
// TODO: This is a quite primitive way to detect ContainerInterface::get.
161167
// Can we somehow get the interface of a given variable?

src/parser/tokens.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use regex::Regex;
12
use std::collections::HashMap;
23
use tree_sitter::Range;
34

@@ -25,6 +26,8 @@ pub enum TokenData {
2526
DrupalServiceDefinition(DrupalService),
2627
DrupalHookReference(String),
2728
DrupalHookDefinition(DrupalHook),
29+
DrupalPermissionDefinition(DrupalPermission),
30+
DrupalPermissionReference(String),
2831
}
2932

3033
#[derive(Debug, PartialEq)]
@@ -82,6 +85,19 @@ pub struct DrupalRoute {
8285
pub defaults: DrupalRouteDefaults,
8386
}
8487

88+
impl DrupalRoute {
89+
pub fn get_route_parameters(&self) -> Vec<&str> {
90+
let re = Regex::new(r"\{([^{}]+)\}");
91+
match re {
92+
Ok(re) => re
93+
.captures_iter(&self.path)
94+
.map(|c| c.get(1).unwrap().as_str())
95+
.collect(),
96+
Err(_) => vec![],
97+
}
98+
}
99+
}
100+
85101
#[derive(Debug)]
86102
pub struct DrupalRouteDefaults {
87103
pub controller: Option<PhpMethod>,
@@ -103,6 +119,12 @@ pub struct DrupalHook {
103119
pub parameters: Option<String>,
104120
}
105121

122+
#[derive(Debug)]
123+
pub struct DrupalPermission {
124+
pub name: String,
125+
pub title: String,
126+
}
127+
106128
#[cfg(test)]
107129
mod tests {
108130
use super::*;

src/parser/yaml.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ use std::{usize, vec};
44
use tree_sitter::{Node, Parser, Point, Tree};
55

66
use super::tokens::{
7-
DrupalRoute, DrupalRouteDefaults, DrupalService, PhpClassName, PhpMethod, Token, TokenData,
7+
DrupalPermission, DrupalRoute, DrupalRouteDefaults, DrupalService, PhpClassName, PhpMethod,
8+
Token, TokenData,
89
};
910

1011
pub struct YamlParser {
1112
source: String,
13+
uri: String,
1214
}
1315

1416
impl YamlParser {
15-
pub fn new(source: &str) -> Self {
17+
pub fn new(source: &str, uri: &str) -> Self {
1618
Self {
1719
source: source.to_string(),
20+
uri: uri.to_string(),
1821
}
1922
}
2023

@@ -95,8 +98,20 @@ impl YamlParser {
9598
let value_node = node.child_by_field_name("value")?;
9699

97100
if let Some(map) = self.get_block_node_map(&value_node) {
101+
// Parse Drupal Permission.
102+
if self.uri.ends_with(".permissions.yml") {
103+
if let Some(title) = map.get("title") {
104+
return Some(Token::new(
105+
TokenData::DrupalPermissionDefinition(DrupalPermission {
106+
name: key.to_string(),
107+
title: self.get_node_text(title).to_string(),
108+
}),
109+
node.range(),
110+
));
111+
}
112+
}
98113
// Parse Drupal Route.
99-
if let (Some(path), Some(defaults)) = (map.get("path"), map.get("defaults")) {
114+
else if let (Some(path), Some(defaults)) = (map.get("path"), map.get("defaults")) {
100115
return Some(Token::new(
101116
TokenData::DrupalRouteDefinition(DrupalRoute {
102117
name: key.to_string(),
@@ -132,6 +147,18 @@ impl YamlParser {
132147
TokenData::PhpClassReference(PhpClassName::from(self.get_node_text(&value_node))),
133148
value_node.range(),
134149
)),
150+
"_permission" => Some(Token::new(
151+
TokenData::DrupalPermissionReference(
152+
self.get_node_text(&value_node).to_string().replace("'", ""),
153+
),
154+
value_node.range(),
155+
)),
156+
"route_name" => Some(Token::new(
157+
TokenData::DrupalRouteReference(
158+
self.get_node_text(&value_node).to_string().replace("'", ""),
159+
),
160+
value_node.range(),
161+
)),
135162
"arguments" => {
136163
let argument = value_node.descendant_for_point_range(point?, point?)?;
137164
if argument.kind() != "single_quote_scalar" {

0 commit comments

Comments
 (0)