From f9185d361e9d524f244dc5fa230d5289cb1b2f7d Mon Sep 17 00:00:00 2001 From: Ales Brelih Date: Fri, 5 Apr 2024 11:36:48 +0200 Subject: [PATCH] feat: jump to def for variables It lists all variables in jobs it founds when traveling through the extends. --- src/gitlab_ci_ls_parser/handlers.rs | 32 +++++ src/gitlab_ci_ls_parser/mod.rs | 6 +- src/gitlab_ci_ls_parser/parser.rs | 75 ++++++++++++ src/gitlab_ci_ls_parser/parser_utils.rs | 16 +++ src/gitlab_ci_ls_parser/treesitter.rs | 150 +++++++++++++++++++++++- 5 files changed, 274 insertions(+), 5 deletions(-) diff --git a/src/gitlab_ci_ls_parser/handlers.rs b/src/gitlab_ci_ls_parser/handlers.rs index 4a3f68d..35b89a0 100644 --- a/src/gitlab_ci_ls_parser/handlers.rs +++ b/src/gitlab_ci_ls_parser/handlers.rs @@ -249,6 +249,38 @@ impl LSPHandlers { } } } + parser::PositionType::Variable => { + let line = document.lines().nth(position.line as usize)?; + let word = + parser_utils::ParserUtils::extract_variable(line, position.character as usize)?; + + let variable_locations = self.parser.get_variable_definitions( + word, + document_uri.as_str(), + position, + store, + )?; + + for location in variable_locations { + locations.push(LSPLocation { + uri: location.uri, + range: location.range, + }); + } + let mut root = self + .variables + .lock() + .unwrap() + .iter() + .filter(|(name, _)| name.starts_with(word)) + .map(|(_, el)| LSPLocation { + uri: el.uri.clone(), + range: el.range.clone(), + }) + .collect::>(); + + locations.append(&mut root); + } _ => { error!("invalid position type for goto def"); return None; diff --git a/src/gitlab_ci_ls_parser/mod.rs b/src/gitlab_ci_ls_parser/mod.rs index 5f61903..e39440c 100644 --- a/src/gitlab_ci_ls_parser/mod.rs +++ b/src/gitlab_ci_ls_parser/mod.rs @@ -9,13 +9,13 @@ pub mod parser; pub mod parser_utils; pub mod treesitter; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct LSPPosition { pub line: u32, pub character: u32, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Range { pub start: LSPPosition, pub end: LSPPosition, @@ -79,7 +79,7 @@ pub struct GitlabFile { pub content: String, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct GitlabElement { pub key: String, pub content: Option, diff --git a/src/gitlab_ci_ls_parser/parser.rs b/src/gitlab_ci_ls_parser/parser.rs index f24fb95..d50f80b 100644 --- a/src/gitlab_ci_ls_parser/parser.rs +++ b/src/gitlab_ci_ls_parser/parser.rs @@ -33,6 +33,13 @@ pub trait Parser { _follow: bool, iteration: i32, ) -> Option<()>; + fn get_variable_definitions( + &self, + word: &str, + uri: &str, + position: Position, + store: &HashMap, + ) -> Option>; } #[allow(clippy::module_name_repetitions)] @@ -65,6 +72,45 @@ impl ParserImpl { } } + fn all_nodes( + &self, + store: &HashMap, + all_nodes: &mut Vec, + node: GitlabElement, + iter: usize, + ) { + // Another safety wow + if iter > 5 { + return; + } + + all_nodes.push(node.clone()); + + let extends = + self.get_all_extends(node.uri, node.content.unwrap_or_default().as_str(), None); + + if extends.is_empty() { + return; + } + + for extend in extends { + for (uri, content) in store { + if let Some(root_node) = self.get_root_node(uri, content, extend.key.as_str()) { + let node = GitlabElement { + uri: root_node.uri, + key: root_node.key, + content: Some(root_node.content.unwrap()), + ..Default::default() + }; + + self.all_nodes(store, all_nodes, node, iter + 1); + + break; + } + } + } + } + fn parse_remote_files(&self, parse_results: &mut ParseResults, remote_files: &[GitlabFile]) { for remote_file in remote_files { parse_results.nodes.append( @@ -302,4 +348,33 @@ impl Parser for ParserImpl { ) -> Vec { self.treesitter.get_all_job_needs(uri, content, needs_name) } + + fn get_variable_definitions( + &self, + variable: &str, + uri: &str, + position: Position, + store: &HashMap, + ) -> Option> { + let mut all_nodes = vec![]; + + if let Some(content) = store.get(uri) { + let element = self + .treesitter + .get_root_node_at_position(content, position)?; + + self.all_nodes(store, &mut all_nodes, element, 0); + } + + Some( + all_nodes + .iter() + .filter_map(|e| { + let cnt = store.get(&e.uri)?; + self.treesitter + .job_variable_definition(e.uri.as_str(), cnt, variable, &e.key) + }) + .collect(), + ) + } } diff --git a/src/gitlab_ci_ls_parser/parser_utils.rs b/src/gitlab_ci_ls_parser/parser_utils.rs index dd11e9f..0583927 100644 --- a/src/gitlab_ci_ls_parser/parser_utils.rs +++ b/src/gitlab_ci_ls_parser/parser_utils.rs @@ -63,4 +63,20 @@ impl ParserUtils { uri.hash(&mut hasher); hasher.finish().to_string() } + + pub fn extract_variable(line: &str, char_index: usize) -> Option<&str> { + if char_index >= line.len() { + return None; + } + + let start = line[..char_index] + .rfind(|c: char| c == '$' || c == '{') + .map_or(0, |index| index + 1); + + let end = line[char_index..] + .find(|c: char| !c.is_alphabetic() && c != '_') + .map_or(line.len(), |index| index + char_index); + + Some(&line[start..end]) + } } diff --git a/src/gitlab_ci_ls_parser/treesitter.rs b/src/gitlab_ci_ls_parser/treesitter.rs index dd12283..60585c7 100644 --- a/src/gitlab_ci_ls_parser/treesitter.rs +++ b/src/gitlab_ci_ls_parser/treesitter.rs @@ -28,6 +28,15 @@ pub trait Treesitter { needs_name: Option<&str>, ) -> Vec; fn get_position_type(&self, content: &str, position: Position) -> parser::PositionType; + fn get_root_node_at_position(&self, content: &str, position: Position) + -> Option; + fn job_variable_definition( + &self, + uri: &str, + content: &str, + variable_name: &str, + job_name: &str, + ) -> Option; } #[allow(clippy::module_name_repetitions)] @@ -694,8 +703,6 @@ impl Treesitter for TreesitterImpl { // together but there are multiple capture groups I need to iterate over // TODO: check treesitter if I can group to make this easier.. Perhaps some capture // group is wrong. - error!("CAPTURES: {:?}", mat.captures); - error!("position: {:?}", position.line); let remote_include_indexes: Vec = vec![10, 11, 12, 13, 14, 15, 16, 17]; if mat .captures @@ -872,4 +879,143 @@ impl Treesitter for TreesitterImpl { needs } + + fn get_root_node_at_position( + &self, + content: &str, + position: Position, + ) -> Option { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(tree_sitter_yaml::language()) + .expect("Error loading YAML grammar"); + + let query_source = r" + ( + stream( + document( + block_node( + block_mapping( + block_mapping_pair + key: (flow_node(plain_scalar(string_scalar)@key)) + )@full + ) + ) + ) + ) + "; + + let tree = parser.parse(content, None).unwrap(); + let root_node = tree.root_node(); + + let query = Query::new(language(), query_source).unwrap(); + + let mut cursor_qry = QueryCursor::new(); + let matches = cursor_qry.matches(&query, root_node, content.as_bytes()); + + matches + .into_iter() + .flat_map(|m| m.captures.iter()) + .find(|c| { + c.index == 1 + && c.node.start_position().row <= position.line as usize + && c.node.end_position().row >= position.line as usize + }) + .map(|c| { + let text = content[c.node.byte_range()].to_string(); + let key = text.lines().collect::>()[0] + .trim_end_matches(':') + .to_string(); + + GitlabElement { + key, + content: Some(text), + ..Default::default() + } + }) + } + + fn job_variable_definition( + &self, + uri: &str, + content: &str, + variable_name: &str, + job_name: &str, + ) -> Option { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(tree_sitter_yaml::language()) + .expect("Error loading YAML grammar"); + + let query_source = format!( + r#" + ( + stream( + document( + block_node( + block_mapping( + block_mapping_pair + key: (flow_node(plain_scalar(string_scalar)@key)) + value: ( + block_node( + block_mapping( + block_mapping_pair + key: (flow_node(plain_scalar(string_scalar)@property_key)) + value: ( + block_node( + block_mapping( + block_mapping_pair + key: (flow_node(plain_scalar(string_scalar)@variable_key)) + ) + ) + ) + (#eq? @property_key "variables") + ) + ) + ) + ) + ) + ) + ) + (#eq? @key "{job_name}") + (#eq? @variable_key "{variable_name}") + ) + "# + ); + + let tree = parser.parse(content, None).unwrap(); + let root_node = tree.root_node(); + + let query = Query::new(language(), &query_source).unwrap(); + + let mut cursor_qry = QueryCursor::new(); + let matches = cursor_qry.matches(&query, root_node, content.as_bytes()); + + matches + .into_iter() + .flat_map(|m| m.captures.iter()) + .find(|c| c.index == 2) + .map(|c| { + let text = content[c.node.byte_range()].to_string(); + let key = text.lines().collect::>()[0] + .trim_end_matches(':') + .to_string(); + + GitlabElement { + uri: uri.to_string(), + key, + content: Some(text), + range: Range { + start: LSPPosition { + line: u32::try_from(c.node.start_position().row).unwrap_or(0), + character: u32::try_from(c.node.start_position().column).unwrap_or(0), + }, + end: LSPPosition { + line: u32::try_from(c.node.end_position().row).unwrap_or(0), + character: u32::try_from(c.node.end_position().column).unwrap_or(0), + }, + }, + } + }) + } }