Skip to content

Commit

Permalink
Bump tree-sitter-r 2 - Add support for StringContent and `EscapeSeq…
Browse files Browse the repository at this point in the history
…uence` nodes (#512)

* Add support for `StringContent` and `EscapeSequence` nodes

* Use existing `ancestors()` helper

* Use `node_in_string()` helper
  • Loading branch information
DavisVaughan authored Sep 16, 2024
1 parent 398cfb9 commit ac9f0e0
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/ark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ stdext = { path = "../stdext" }
tokio = { version = "1.26.0", features = ["full"] }
tower-lsp = "0.19.0"
tree-sitter = "0.22.6"
tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "bc8919d3c38b816652e1e2d1a1be037cf74364cb" }
tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "9d1a68f8f239bc3749a481ac85e2163e24f6362c" }
uuid = "1.3.0"
url = "2.4.1"
walkdir = "2"
Expand Down
5 changes: 3 additions & 2 deletions crates/ark/src/lsp/completions/sources/composite/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::lsp::indexer;
use crate::lsp::state::WorldState;
use crate::lsp::traits::rope::RopeExt;
use crate::lsp::traits::string::StringExt;
use crate::treesitter::node_in_string;
use crate::treesitter::NodeTypeExt;

pub(super) fn completions_from_workspace(
Expand All @@ -41,8 +42,8 @@ pub(super) fn completions_from_workspace(
}
}

if node.is_string() {
log::error!("Should have already been handled by file path completions source");
if node_in_string(&node) {
log::error!("Should have already been handled by string completions source");
return Ok(None);
}

Expand Down
3 changes: 2 additions & 1 deletion crates/ark/src/lsp/completions/sources/unique/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::lsp::completions::sources::utils::CallNodePositionType;
use crate::lsp::completions::types::CompletionData;
use crate::lsp::document_context::DocumentContext;
use crate::lsp::signature_help::r_signature_help;
use crate::treesitter::node_in_string;
use crate::treesitter::NodeTypeExt;

pub fn completions_from_custom_source(
Expand Down Expand Up @@ -190,7 +191,7 @@ pub fn completions_from_custom_source_impl(
continue;
});

if enquote && !node.is_string() {
if enquote && !node_in_string(&node) {
item.insert_text = Some(format!("\"{value}\""));
} else {
let mut insert_text = sym_quote_invalid(value.as_str());
Expand Down
8 changes: 3 additions & 5 deletions crates/ark/src/lsp/completions/sources/unique/file_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ use harp::utils::r_normalize_path;
use stdext::unwrap;
use stdext::IntoResult;
use tower_lsp::lsp_types::CompletionItem;
use tree_sitter::Node;

use crate::lsp::completions::completion_item::completion_item_from_direntry;
use crate::lsp::completions::sources::utils::set_sort_text_by_words_first;
use crate::lsp::document_context::DocumentContext;
use crate::lsp::traits::rope::RopeExt;

pub(super) fn completions_from_string_file_path(
node: &Node,
context: &DocumentContext,
) -> Result<Vec<CompletionItem>> {
log::info!("completions_from_string_file_path()");
Expand All @@ -33,11 +35,7 @@ pub(super) fn completions_from_string_file_path(
// NOTE: This includes the quotation characters on the string, and so
// also includes any internal escapes! We need to decode the R string
// before searching the path entries.
let token = context
.document
.contents
.node_slice(&context.node)?
.to_string();
let token = context.document.contents.node_slice(&node)?.to_string();
let contents = unsafe { r_string_decode(token.as_str()).into_result()? };
log::info!("String value (decoded): {}", contents);

Expand Down
30 changes: 15 additions & 15 deletions crates/ark/src/lsp/completions/sources/unique/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ use tower_lsp::lsp_types::CompletionItem;
use super::file_path::completions_from_string_file_path;
use crate::lsp::completions::sources::unique::subset::completions_from_string_subset;
use crate::lsp::document_context::DocumentContext;
use crate::treesitter::NodeTypeExt;
use crate::treesitter::node_find_string;

pub fn completions_from_string(context: &DocumentContext) -> Result<Option<Vec<CompletionItem>>> {
log::info!("completions_from_string()");

let node = context.node;

if !node.is_string() {
// Find actual `NodeType::String` node. Needed in case we are in its children.
let Some(node) = node_find_string(&node) else {
return Ok(None);
}
};

// Must actually be "inside" the string, so these places don't count, even
// though they are detected as part of the string nodes `|""|`
Expand All @@ -41,48 +42,49 @@ pub fn completions_from_string(context: &DocumentContext) -> Result<Option<Vec<C

// Check if we are doing string subsetting, like `x["<tab>"]`. This is a very unique
// case that takes priority over file path completions.
if let Some(mut candidates) = completions_from_string_subset(context)? {
if let Some(mut candidates) = completions_from_string_subset(&node, context)? {
completions.append(&mut candidates);
return Ok(Some(completions));
}

// If no special string cases are hit, we show file path completions
completions.append(&mut completions_from_string_file_path(context)?);
completions.append(&mut completions_from_string_file_path(&node, context)?);

Ok(Some(completions))
}

#[cfg(test)]
mod tests {
use harp::assert_match;
use tree_sitter::Point;

use crate::lsp::completions::sources::completions_from_unique_sources;
use crate::lsp::completions::sources::unique::string::completions_from_string;
use crate::lsp::document_context::DocumentContext;
use crate::lsp::documents::Document;
use crate::test::point_from_cursor;
use crate::test::r_test;
use crate::treesitter::node_find_string;
use crate::treesitter::NodeTypeExt;

#[test]
fn test_outside_quotes() {
r_test(|| {
// Before or after the `''`, i.e. `|''` or `''|`.
// Still considered part of the string node.
let point = Point { row: 0, column: 0 };
let document = Document::new("''", None);
let (text, point) = point_from_cursor("@''");
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);

assert!(context.node.is_string());
assert!(node_find_string(&context.node).is_some());
assert_eq!(completions_from_string(&context).unwrap(), None);
})
}

#[test]
fn test_not_string() {
r_test(|| {
let point = Point { row: 0, column: 0 };
let document = Document::new("foo", None);
let (text, point) = point_from_cursor("@foo");
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);

assert!(context.node.is_identifier());
Expand All @@ -93,12 +95,10 @@ mod tests {
#[test]
fn test_trigger() {
r_test(|| {
// Before or after the `''`, i.e. `|''` or `''|`.
// Still considered part of the string node.
let point = Point { row: 0, column: 2 };
let (text, point) = point_from_cursor("'~/@'");

// Assume home directory is not empty
let document = Document::new("'~/'", None);
let document = Document::new(text.as_str(), None);

// `None` trigger -> Return file completions
let context = DocumentContext::new(&document, point, None);
Expand Down
57 changes: 37 additions & 20 deletions crates/ark/src/lsp/completions/sources/unique/subset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::treesitter::NodeTypeExt;
/// of `""`, enquotes its completion items, and is composite so it meshes with other
/// generic completions. We consider this a completely different path.
pub(super) fn completions_from_string_subset(
node: &Node,
context: &DocumentContext,
) -> Result<Option<Vec<CompletionItem>>> {
log::info!("completions_from_string_subset()");
Expand All @@ -37,7 +38,7 @@ pub(super) fn completions_from_string_subset(
const ENQUOTE: bool = false;

// i.e. find `x` in `x[""]` or `x[c("foo", "")]`
let Some(node) = node_find_object_for_string_subset(context.node, context) else {
let Some(node) = node_find_object_for_string_subset(node, context) else {
return Ok(None);
};

Expand All @@ -56,14 +57,14 @@ pub(super) fn completions_from_string_subset(
}

fn node_find_object_for_string_subset<'tree>(
mut node: Node<'tree>,
node: &Node<'tree>,
context: &DocumentContext,
) -> Option<Node<'tree>> {
if !node.is_string() {
return None;
}

node = match node_find_parent_call(node) {
let mut node = match node_find_parent_call(node) {
Some(node) => node,
None => return None,
};
Expand All @@ -74,7 +75,7 @@ fn node_find_object_for_string_subset<'tree>(
return None;
}

node = match node_find_parent_call(node) {
node = match node_find_parent_call(&node) {
Some(node) => node,
None => return None,
};
Expand Down Expand Up @@ -105,7 +106,7 @@ fn node_find_object_for_string_subset<'tree>(
return Some(node);
}

fn node_find_parent_call(x: Node) -> Option<Node> {
fn node_find_parent_call<'tree>(x: &Node<'tree>) -> Option<Node<'tree>> {
// Find the `Argument` node
let Some(x) = x.parent() else {
return None;
Expand Down Expand Up @@ -165,6 +166,7 @@ mod tests {
use crate::lsp::documents::Document;
use crate::test::point_from_cursor;
use crate::test::r_test;
use crate::treesitter::node_find_string;

#[test]
fn test_string_subset_completions() {
Expand All @@ -176,8 +178,11 @@ mod tests {
let (text, point) = point_from_cursor(r#"foo["@"]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let node = node_find_string(&context.node).unwrap();

let completions = completions_from_string_subset(&context).unwrap().unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);

let completion = completions.get(0).unwrap();
Expand All @@ -194,28 +199,40 @@ mod tests {
let (text, point) = point_from_cursor(r#"foo[["@"]]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap().unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);

// Inside `""` as second argument
let (text, point) = point_from_cursor(r#"foo[, "@"]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap().unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);

// Inside `""` inside `c()`
let (text, point) = point_from_cursor(r#"foo[c("@")]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap().unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);

// Inside `""` inside `c()` with another string already specified
let (text, point) = point_from_cursor(r#"foo[c("a", "@")]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap().unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);

// Inside `""` inside `fn()` - no completions from string subset!
Expand All @@ -224,22 +241,19 @@ mod tests {
let (text, point) = point_from_cursor(r#"foo[fn("@")]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap();
assert!(completions.is_none());

// Right before the `[`
let (text, point) = point_from_cursor(r#"foo@[""]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context).unwrap();
assert!(completions.is_none());

// A fake object that we can't get object names for.
// It _looks_ like we want string completions though, so we return an empty set.
let (text, point) = point_from_cursor(r#"not_real["@"]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_string_subset(&context).unwrap().unwrap();
let node = node_find_string(&context.node).unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert!(completions.is_empty());

// Clean up
Expand All @@ -257,8 +271,11 @@ mod tests {
let (text, point) = point_from_cursor(r#"foo[, "@"]"#);
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let node = node_find_string(&context.node).unwrap();

let completions = completions_from_string_subset(&context).unwrap().unwrap();
let completions = completions_from_string_subset(&node, &context)
.unwrap()
.unwrap();
assert_eq!(completions.len(), 2);
assert_eq!(completions.get(0).unwrap().label, "a".to_string());
assert_eq!(completions.get(1).unwrap().label, "b".to_string());
Expand Down
21 changes: 21 additions & 0 deletions crates/ark/src/treesitter.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use tree_sitter::Node;

use crate::lsp::traits::node::NodeExt;
use crate::lsp::traits::rope::RopeExt;

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -27,6 +28,8 @@ pub enum NodeType {
Complex,
Float,
String,
StringContent,
EscapeSequence,
Identifier,
DotDotI,
Dots,
Expand Down Expand Up @@ -71,6 +74,8 @@ fn node_type(x: &Node) -> NodeType {
"complex" => NodeType::Complex,
"float" => NodeType::Float,
"string" => NodeType::String,
"string_content" => NodeType::StringContent,
"escape_sequence" => NodeType::EscapeSequence,
"identifier" => NodeType::Identifier,
"dot_dot_i" => NodeType::DotDotI,
"dots" => NodeType::Dots,
Expand Down Expand Up @@ -410,6 +415,22 @@ pub(crate) fn node_text(node: &Node, contents: &ropey::Rope) -> Option<String> {
contents.node_slice(node).ok().map(|f| f.to_string())
}

pub(crate) fn node_find_string<'a>(node: &'a Node) -> Option<Node<'a>> {
if node.is_string() {
// Already on a string
return Some(*node);
}
// If we are on one of the following, we return the string parent:
// - Anonymous node inside a string, like `"'"`
// - `NodeType::StringContent`
// - `NodeType::EscapeSequence`
node.ancestors().find(|parent| parent.is_string())
}

pub(crate) fn node_in_string(node: &Node) -> bool {
node_find_string(node).is_some()
}

pub(crate) fn node_is_call(node: &Node, name: &str, contents: &ropey::Rope) -> bool {
if !node.is_call() {
return false;
Expand Down

0 comments on commit ac9f0e0

Please sign in to comment.