diff --git a/crates/ark/src/lsp/completions/sources/composite/call.rs b/crates/ark/src/lsp/completions/sources/composite/call.rs index 49e241872..303ba4dd6 100644 --- a/crates/ark/src/lsp/completions/sources/composite/call.rs +++ b/crates/ark/src/lsp/completions/sources/composite/call.rs @@ -267,6 +267,7 @@ fn completions_from_workspace_arguments( return Ok(None); }, indexer::IndexEntryData::Variable { .. } => return Ok(None), + indexer::IndexEntryData::Method { .. } => return Ok(None), } // Only 1 call worth of arguments are added to the completion set. diff --git a/crates/ark/src/lsp/completions/sources/composite/workspace.rs b/crates/ark/src/lsp/completions/sources/composite/workspace.rs index 945c40a11..4d2245839 100644 --- a/crates/ark/src/lsp/completions/sources/composite/workspace.rs +++ b/crates/ark/src/lsp/completions/sources/composite/workspace.rs @@ -125,6 +125,9 @@ fn completions_from_workspace( }; completions.push(completion); }, + + // Methods are currently only indexed for workspace symbols + indexer::IndexEntryData::Method { .. } => {}, } }); diff --git a/crates/ark/src/lsp/indexer.rs b/crates/ark/src/lsp/indexer.rs index 34f018c7b..5f566a557 100644 --- a/crates/ark/src/lsp/indexer.rs +++ b/crates/ark/src/lsp/indexer.rs @@ -39,6 +39,10 @@ pub enum IndexEntryData { name: String, arguments: Vec, }, + // Like Function but not used for completions yet + Method { + name: String, + }, Section { level: usize, title: String, @@ -198,127 +202,173 @@ fn index_document(document: &Document, path: &Path) { let root = ast.root_node(); let mut cursor = root.walk(); + let mut entries = Vec::new(); + for node in root.children(&mut cursor) { - if let Err(err) = match index_node(path, contents, &node) { - Ok(Some(entry)) => insert(path, entry), - Ok(None) => Ok(()), - Err(err) => Err(err), - } { + if let Err(err) = index_node(path, contents, &node, &mut entries) { lsp::log_error!("Can't index document: {err:?}"); } } -} - -fn index_node(path: &Path, contents: &Rope, node: &Node) -> anyhow::Result> { - if let Ok(Some(entry)) = index_function(path, contents, node) { - return Ok(Some(entry)); - } - // Should be after function indexing as this is a more general case - if let Ok(Some(entry)) = index_variable(path, contents, node) { - return Ok(Some(entry)); - } - - if let Ok(Some(entry)) = index_comment(path, contents, node) { - return Ok(Some(entry)); + for entry in entries { + if let Err(err) = insert(path, entry) { + lsp::log_error!("Can't insert index entry: {err:?}"); + } } - - Ok(None) } -fn index_function( - _path: &Path, +fn index_node( + path: &Path, contents: &Rope, node: &Node, -) -> anyhow::Result> { - // Check for assignment. - matches!( - node.node_type(), - NodeType::BinaryOperator(BinaryOperatorType::LeftAssignment) | - NodeType::BinaryOperator(BinaryOperatorType::EqualsAssignment) - ) - .into_result()?; - - // Check for identifier on left-hand side. - let lhs = node.child_by_field_name("lhs").into_result()?; - lhs.is_identifier_or_string().into_result()?; - - // Check for a function definition on the right-hand side. - let rhs = node.child_by_field_name("rhs").into_result()?; - rhs.is_function_definition().into_result()?; - - let name = contents.node_slice(&lhs)?.to_string(); - let mut arguments = Vec::new(); - - // Get the parameters node. - let parameters = rhs.child_by_field_name("parameters").into_result()?; - - // Iterate through each, and get the names. - let mut cursor = parameters.walk(); - for child in parameters.children(&mut cursor) { - let name = unwrap!(child.child_by_field_name("name"), None => continue); - if name.is_identifier() { - let name = contents.node_slice(&name)?.to_string(); - arguments.push(name); - } - } - - let start = convert_point_to_position(contents, lhs.start_position()); - let end = convert_point_to_position(contents, lhs.end_position()); - - Ok(Some(IndexEntry { - key: name.clone(), - range: Range { start, end }, - data: IndexEntryData::Function { - name: name.clone(), - arguments, - }, - })) + entries: &mut Vec, +) -> anyhow::Result<()> { + index_assignment(path, contents, node, entries)?; + index_comment(path, contents, node, entries)?; + Ok(()) } -fn index_variable( - _path: &Path, +fn index_assignment( + path: &Path, contents: &Rope, node: &Node, -) -> anyhow::Result> { + entries: &mut Vec, +) -> anyhow::Result<()> { if !matches!( node.node_type(), NodeType::BinaryOperator(BinaryOperatorType::LeftAssignment) | NodeType::BinaryOperator(BinaryOperatorType::EqualsAssignment) ) { - return Ok(None); + return Ok(()); } - let Some(lhs) = node.child_by_field_name("lhs") else { - return Ok(None); + let lhs = match node.child_by_field_name("lhs") { + Some(lhs) => lhs, + None => return Ok(()), + }; + + let Some(rhs) = node.child_by_field_name("rhs") else { + return Ok(()); }; + if crate::treesitter::node_is_call(&rhs, "R6Class", contents) || + crate::treesitter::node_is_namespaced_call(&rhs, "R6", "R6Class", contents) + { + index_r6_class(path, contents, &rhs, entries)?; + } + let lhs_text = contents.node_slice(&lhs)?.to_string(); - // Super hacky but let's wait until the typed API to do better + // The method matching is super hacky but let's wait until the typed API to + // do better if !lhs_text.starts_with("method(") && !lhs.is_identifier_or_string() { - return Ok(None); + return Ok(()); + } + + let Some(rhs) = node.child_by_field_name("rhs") else { + return Ok(()); + }; + + if rhs.is_function_definition() { + // If RHS is a function definition, emit a function symbol + let mut arguments = Vec::new(); + if let Some(parameters) = rhs.child_by_field_name("parameters") { + let mut cursor = parameters.walk(); + for child in parameters.children(&mut cursor) { + let name = unwrap!(child.child_by_field_name("name"), None => continue); + if name.is_identifier() { + let name = contents.node_slice(&name)?.to_string(); + arguments.push(name); + } + } + } + + let start = convert_point_to_position(contents, lhs.start_position()); + let end = convert_point_to_position(contents, lhs.end_position()); + + entries.push(IndexEntry { + key: lhs_text.clone(), + range: Range { start, end }, + data: IndexEntryData::Function { + name: lhs_text, + arguments, + }, + }); + } else { + // Otherwise, emit variable + let start = convert_point_to_position(contents, lhs.start_position()); + let end = convert_point_to_position(contents, lhs.end_position()); + entries.push(IndexEntry { + key: lhs_text.clone(), + range: Range { start, end }, + data: IndexEntryData::Variable { name: lhs_text }, + }); } - let start = convert_point_to_position(contents, lhs.start_position()); - let end = convert_point_to_position(contents, lhs.end_position()); + Ok(()) +} + +fn index_r6_class( + _path: &Path, + contents: &Rope, + node: &Node, + entries: &mut Vec, +) -> anyhow::Result<()> { + // Tree-sitter query to match individual methods in R6Class public/private lists + let query_str = r#" + (argument + name: (identifier) @access + value: (call + function: (identifier) @_list_fn + arguments: (arguments + (argument + name: (identifier) @method_name + value: (function_definition) @method_fn + ) + ) + ) + (#match? @access "public|private") + (#eq? @_list_fn "list") + ) + "#; + let mut ts_query = crate::treesitter::TSQuery::new(query_str)?; + + // We'll switch from Rope to String in the near future so let's not + // worry about this conversion now + let contents_str = contents.to_string(); + + for method_node in ts_query.captures_for(*node, "method_name", contents_str.as_bytes()) { + let name = contents.node_slice(&method_node)?.to_string(); + let start = convert_point_to_position(contents, method_node.start_position()); + let end = convert_point_to_position(contents, method_node.end_position()); + + entries.push(IndexEntry { + key: name.clone(), + range: Range { start, end }, + data: IndexEntryData::Method { name }, + }); + } - Ok(Some(IndexEntry { - key: lhs_text.clone(), - range: Range { start, end }, - data: IndexEntryData::Variable { name: lhs_text }, - })) + Ok(()) } -fn index_comment(_path: &Path, contents: &Rope, node: &Node) -> anyhow::Result> { +fn index_comment( + _path: &Path, + contents: &Rope, + node: &Node, + entries: &mut Vec, +) -> anyhow::Result<()> { // check for comment - node.is_comment().into_result()?; + if !node.is_comment() { + return Ok(()); + } // see if it looks like a section let comment = contents.node_slice(node)?.to_string(); - let matches = RE_COMMENT_SECTION - .captures(comment.as_str()) - .into_result()?; + let matches = match RE_COMMENT_SECTION.captures(comment.as_str()) { + Some(m) => m, + None => return Ok(()), + }; let level = matches.get(1).into_result()?; let title = matches.get(2).into_result()?; @@ -328,15 +378,152 @@ fn index_comment(_path: &Path, contents: &Rope, node: &Node) -> anyhow::Result { + let doc = Document::new($code, None); + let path = PathBuf::from("/path/to/file.R"); + let root = doc.ast.root_node(); + let mut cursor = root.walk(); + + let mut entries = vec![]; + for node in root.children(&mut cursor) { + let _ = index_node(&path, &doc.contents, &node, &mut entries); + } + assert_debug_snapshot!(entries); + }; + } + + // Note that unlike document symbols whose ranges cover the whole entity + // they represent, the range of workspace symbols only cover the identifers + + #[test] + fn test_index_function() { + test_index!( + r#" +my_function <- function(a, b = 1) { + a + b + + # These are not indexed as workspace symbol + inner <- function() { + 2 + } + inner_var <- 3 +} + +my_variable <- 1 +"# + ); + } + + #[test] + fn test_index_variable() { + test_index!( + r#" +x <- 10 +y = "hello" +"# + ); + } + + #[test] + fn test_index_s7_methods() { + test_index!( + r#" +Class <- new_class("Class") +generic <- new_generic("generic", "arg", + function(arg) { + S7_dispatch() + } +) +method(generic, Class) <- function(arg) { + NULL +} +"# + ); + } + + #[test] + fn test_index_comment_section() { + test_index!( + r#" +# Section 1 ---- +x <- 10 + +## Subsection ====== +y <- 20 + +x <- function() { + # This inner section is not indexed ---- +} + +"# + ); + } + + #[test] + fn test_index_r6class() { + test_index!( + r#" +class <- R6Class( + public = list( + initialize = function() { + 1 + }, + public_method = function() { + 2 + } + ), + private = list( + private_method = function() { + 1 + } + ), + other = list( + other_method = function() { + 1 + } + ) +) +"# + ); + } + + #[test] + fn test_index_r6class_namespaced() { + test_index!( + r#" +class <- R6::R6Class( + public = list( + initialize = function() { + 1 + }, + ) +) +"# + ); + } } diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_comment_section.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_comment_section.snap new file mode 100644 index 000000000..c33dec8c9 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_comment_section.snap @@ -0,0 +1,89 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "Section 1", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 16, + }, + }, + data: Section { + level: 1, + title: "Section 1", + }, + }, + IndexEntry { + key: "x", + range: Range { + start: Position { + line: 2, + character: 0, + }, + end: Position { + line: 2, + character: 1, + }, + }, + data: Variable { + name: "x", + }, + }, + IndexEntry { + key: "Subsection", + range: Range { + start: Position { + line: 4, + character: 0, + }, + end: Position { + line: 4, + character: 20, + }, + }, + data: Section { + level: 2, + title: "Subsection", + }, + }, + IndexEntry { + key: "y", + range: Range { + start: Position { + line: 5, + character: 0, + }, + end: Position { + line: 5, + character: 1, + }, + }, + data: Variable { + name: "y", + }, + }, + IndexEntry { + key: "x", + range: Range { + start: Position { + line: 7, + character: 0, + }, + end: Position { + line: 7, + character: 1, + }, + }, + data: Function { + name: "x", + arguments: [], + }, + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_function.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_function.snap new file mode 100644 index 000000000..7c5befa91 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_function.snap @@ -0,0 +1,42 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "my_function", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 11, + }, + }, + data: Function { + name: "my_function", + arguments: [ + "a", + "b", + ], + }, + }, + IndexEntry { + key: "my_variable", + range: Range { + start: Position { + line: 11, + character: 0, + }, + end: Position { + line: 11, + character: 11, + }, + }, + data: Variable { + name: "my_variable", + }, + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class.snap new file mode 100644 index 000000000..61e33e824 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class.snap @@ -0,0 +1,70 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "initialize", + range: Range { + start: Position { + line: 3, + character: 8, + }, + end: Position { + line: 3, + character: 18, + }, + }, + data: Method { + name: "initialize", + }, + }, + IndexEntry { + key: "public_method", + range: Range { + start: Position { + line: 6, + character: 8, + }, + end: Position { + line: 6, + character: 21, + }, + }, + data: Method { + name: "public_method", + }, + }, + IndexEntry { + key: "private_method", + range: Range { + start: Position { + line: 11, + character: 8, + }, + end: Position { + line: 11, + character: 22, + }, + }, + data: Method { + name: "private_method", + }, + }, + IndexEntry { + key: "class", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 5, + }, + }, + data: Variable { + name: "class", + }, + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class_namespaced.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class_namespaced.snap new file mode 100644 index 000000000..ef545f178 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_r6class_namespaced.snap @@ -0,0 +1,38 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "initialize", + range: Range { + start: Position { + line: 3, + character: 8, + }, + end: Position { + line: 3, + character: 18, + }, + }, + data: Method { + name: "initialize", + }, + }, + IndexEntry { + key: "class", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 5, + }, + }, + data: Variable { + name: "class", + }, + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_s7_methods.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_s7_methods.snap new file mode 100644 index 000000000..894f61d6f --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_s7_methods.snap @@ -0,0 +1,57 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "Class", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 5, + }, + }, + data: Variable { + name: "Class", + }, + }, + IndexEntry { + key: "generic", + range: Range { + start: Position { + line: 2, + character: 0, + }, + end: Position { + line: 2, + character: 7, + }, + }, + data: Variable { + name: "generic", + }, + }, + IndexEntry { + key: "method(generic, Class)", + range: Range { + start: Position { + line: 7, + character: 0, + }, + end: Position { + line: 7, + character: 22, + }, + }, + data: Function { + name: "method(generic, Class)", + arguments: [ + "arg", + ], + }, + }, +] diff --git a/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_variable.snap b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_variable.snap new file mode 100644 index 000000000..917e33805 --- /dev/null +++ b/crates/ark/src/lsp/snapshots/ark__lsp__indexer__tests__index_variable.snap @@ -0,0 +1,38 @@ +--- +source: crates/ark/src/lsp/indexer.rs +expression: entries +--- +[ + IndexEntry { + key: "x", + range: Range { + start: Position { + line: 1, + character: 0, + }, + end: Position { + line: 1, + character: 1, + }, + }, + data: Variable { + name: "x", + }, + }, + IndexEntry { + key: "y", + range: Range { + start: Position { + line: 2, + character: 0, + }, + end: Position { + line: 2, + character: 1, + }, + }, + data: Variable { + name: "y", + }, + }, +] diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index bdd31f13b..590907db6 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -95,6 +95,7 @@ pub fn symbols(params: &WorkspaceSymbolParams) -> anyhow::Result { info.push(SymbolInformation { name: name.clone(), @@ -108,6 +109,20 @@ pub fn symbols(params: &WorkspaceSymbolParams) -> anyhow::Result { + info.push(SymbolInformation { + name: name.clone(), + kind: SymbolKind::METHOD, + location: Location { + uri: Url::from_file_path(path).unwrap(), + range: entry.range, + }, + tags: None, + deprecated: None, + container_name: None, + }); + }, }; }); diff --git a/crates/ark/src/treesitter.rs b/crates/ark/src/treesitter.rs index 93e82c70c..02a21305b 100644 --- a/crates/ark/src/treesitter.rs +++ b/crates/ark/src/treesitter.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use tree_sitter::Node; use crate::lsp::traits::node::NodeExt; @@ -465,6 +466,38 @@ pub(crate) fn node_is_call(node: &Node, name: &str, contents: &ropey::Rope) -> b fun == name } +pub(crate) fn node_is_namespaced_call( + node: &Node, + namespace: &str, + name: &str, + contents: &ropey::Rope, +) -> bool { + if !node.is_call() { + return false; + } + + let Some(op) = node.child_by_field_name("function") else { + return false; + }; + if !op.is_namespace_operator() { + return false; + } + + let (Some(node_namespace), Some(node_name)) = + (op.child_by_field_name("lhs"), op.child_by_field_name("rhs")) + else { + return false; + }; + let Some(node_namespace) = node_text(&node_namespace, contents) else { + return false; + }; + let Some(node_name) = node_text(&node_name, contents) else { + return false; + }; + + node_namespace == namespace && node_name == name +} + /// This function takes a [Node] that you suspect might be in a call argument position /// and walks up the tree, looking for the containing call node /// @@ -571,3 +604,42 @@ pub(crate) fn node_find_containing_call<'tree>(node: Node<'tree>) -> Option anyhow::Result { + let language = &tree_sitter_r::LANGUAGE.into(); + let query = tree_sitter::Query::new(language, query_str) + .map_err(|err| anyhow!("Failed to compile query: {err}"))?; + + let cursor = tree_sitter::QueryCursor::new(); + + Ok(Self { query, cursor }) + } + + /// Match query against `contents` and collect all nodes captured with the + /// given capture name + pub(crate) fn captures_for<'tree>( + &mut self, + node: tree_sitter::Node<'tree>, + capture_name: &str, + contents: &[u8], + ) -> Vec> { + let mut result = Vec::new(); + + for m in self.cursor.matches(&self.query, node, contents) { + for cap in m.captures.iter() { + let cap_name = &self.query.capture_names()[cap.index as usize]; + if *cap_name == capture_name { + result.push(cap.node); + } + } + } + + result + } +}