diff --git a/crates/ark/src/lsp/ark_file.rs b/crates/ark/src/lsp/ark_file.rs index 8c4245ecd3..d55c35e332 100644 --- a/crates/ark/src/lsp/ark_file.rs +++ b/crates/ark/src/lsp/ark_file.rs @@ -90,11 +90,7 @@ impl ArkFile { db: &dyn ArkDb, point: tree_sitter::Point, ) -> anyhow::Result { - let line_col = biome_line_index::LineCol { - line: point.row as u32, - col: point.column as u32, - }; - to_proto::position_from_line_col(line_col, self.line_index(db), self.encoding) + lsp_position_from_tree_sitter_point(point, self.line_index(db), self.encoding) } pub(crate) fn lsp_range_from_tree_sitter_range( @@ -102,9 +98,7 @@ impl ArkFile { db: &dyn ArkDb, range: tree_sitter::Range, ) -> anyhow::Result { - let start = self.lsp_position_from_tree_sitter_point(db, range.start_point)?; - let end = self.lsp_position_from_tree_sitter_point(db, range.end_point)?; - Ok(lsp_types::Range::new(start, end)) + lsp_range_from_tree_sitter_range(range, self.line_index(db), self.encoding) } pub(crate) fn tree_sitter_range_from_lsp_range( @@ -126,6 +120,31 @@ impl ArkFile { } } +/// Free functions over `LineIndex` + `PositionEncoding`, so anything holding +/// those two (an `ArkFile` plus its `db`, or a `DocumentContext`) can convert +/// without each carrying its own copy of the logic. +pub(crate) fn lsp_position_from_tree_sitter_point( + point: tree_sitter::Point, + line_index: &biome_line_index::LineIndex, + encoding: PositionEncoding, +) -> anyhow::Result { + let line_col = biome_line_index::LineCol { + line: point.row as u32, + col: point.column as u32, + }; + to_proto::position_from_line_col(line_col, line_index, encoding) +} + +pub(crate) fn lsp_range_from_tree_sitter_range( + range: tree_sitter::Range, + line_index: &biome_line_index::LineIndex, + encoding: PositionEncoding, +) -> anyhow::Result { + let start = lsp_position_from_tree_sitter_point(range.start_point, line_index, encoding)?; + let end = lsp_position_from_tree_sitter_point(range.end_point, line_index, encoding)?; + Ok(lsp_types::Range::new(start, end)) +} + #[cfg(test)] pub(crate) fn test_ark_file(code: &str) -> (oak_db::OakDatabase, ArkFile) { use aether_path::FilePath; diff --git a/crates/ark/src/lsp/completions/completion_item.rs b/crates/ark/src/lsp/completions/completion_item.rs index 8c16b3cf7a..e266f57d5f 100644 --- a/crates/ark/src/lsp/completions/completion_item.rs +++ b/crates/ark/src/lsp/completions/completion_item.rs @@ -36,6 +36,7 @@ use tower_lsp::lsp_types::Range; use tower_lsp::lsp_types::TextEdit; use tree_sitter::Node; +use crate::lsp::ark_file::lsp_position_from_tree_sitter_point; use crate::lsp::completions::function_context::ArgumentsStatus; use crate::lsp::completions::function_context::FunctionContext; use crate::lsp::completions::function_context::FunctionRefUsage; @@ -100,7 +101,7 @@ pub(super) fn completion_item_from_assignment( let lhs = node.child_by_field_name("lhs").into_result()?; let rhs = node.child_by_field_name("rhs").into_result()?; - let label = lhs.node_as_str(&context.document.contents)?.to_string(); + let label = lhs.node_as_str(context.contents)?.to_string(); // TODO: Resolve functions that exist in-document here. let mut item = completion_item(label.clone(), CompletionData::ScopeVariable { @@ -123,7 +124,7 @@ pub(super) fn completion_item_from_assignment( // benefit from the logic in completion_item_from_function() :( if rhs.node_type() == NodeType::FunctionDefinition { if let Some(parameters) = rhs.child_by_field_name("parameters") { - let parameters = parameters.node_as_str(&context.document.contents)?; + let parameters = parameters.node_as_str(context.contents)?; item.detail = Some(join!(label, parameters)); } @@ -644,9 +645,8 @@ fn completion_item_from_dot_dot_dot( item.kind = Some(CompletionItemKind::FIELD); - let position = context - .document - .lsp_position_from_tree_sitter_point(context.point)?; + let position = + lsp_position_from_tree_sitter_point(context.point, context.line_index, context.encoding)?; let range = Range { start: position, diff --git a/crates/ark/src/lsp/completions/function_context.rs b/crates/ark/src/lsp/completions/function_context.rs index c594848bfb..0d6cbceaf7 100644 --- a/crates/ark/src/lsp/completions/function_context.rs +++ b/crates/ark/src/lsp/completions/function_context.rs @@ -10,6 +10,8 @@ use tower_lsp::lsp_types; use tower_lsp::lsp_types::Range; use tree_sitter::Node; +use crate::lsp::ark_file::lsp_position_from_tree_sitter_point; +use crate::lsp::ark_file::lsp_range_from_tree_sitter_range; use crate::lsp::document_context::DocumentContext; use crate::lsp::traits::node::NodeExt; use crate::treesitter::node_find_parent_call; @@ -61,9 +63,11 @@ impl FunctionContext { // We shouldn't ever attempt to instantiate a FunctionContext or // function-flavored CompletionItem in this degenerate case, but we // return a dummy FunctionContext just to be safe. - let node_end = document_context - .document - .lsp_position_from_tree_sitter_point(completion_node.range().end_point)?; + let node_end = lsp_position_from_tree_sitter_point( + completion_node.range().end_point, + document_context.line_index, + document_context.encoding, + )?; return Ok(Self { name: String::new(), @@ -74,10 +78,7 @@ impl FunctionContext { }); }; - let usage = determine_function_usage( - &effective_function_node, - &document_context.document.contents, - ); + let usage = determine_function_usage(&effective_function_node, document_context.contents); let function_name_node = if effective_function_node.is_namespace_operator() { // Note: this could be 'None', in the case of, e.g., `dplyr::@` @@ -93,7 +94,7 @@ impl FunctionContext { let name = match function_name_node { Some(node) => node - .node_to_string(&document_context.document.contents) + .node_to_string(document_context.contents) .unwrap_or_default(), None => String::new(), }; @@ -111,16 +112,18 @@ impl FunctionContext { Ok(Self { name, range: match function_name_node { - Some(node) => document_context - .document - .lsp_range_from_tree_sitter_range(node.range())?, + Some(node) => lsp_range_from_tree_sitter_range( + node.range(), + document_context.line_index, + document_context.encoding, + )?, None => { // Create a zero-width range at the end of the effective_function_node - let node_end = document_context - .document - .lsp_position_from_tree_sitter_point( - effective_function_node.range().end_point, - )?; + let node_end = lsp_position_from_tree_sitter_point( + effective_function_node.range().end_point, + document_context.line_index, + document_context.encoding, + )?; lsp_types::Range::new(node_end, node_end) }, }, diff --git a/crates/ark/src/lsp/completions/provide.rs b/crates/ark/src/lsp/completions/provide.rs index 44e6e64f73..6006d1efad 100644 --- a/crates/ark/src/lsp/completions/provide.rs +++ b/crates/ark/src/lsp/completions/provide.rs @@ -25,7 +25,7 @@ pub(crate) fn provide_completions( "provide_completions() - Completion node text: '{node_text}', Node type: '{node_type:?}'", node_text = document_context .node - .node_as_str(&document_context.document.contents) + .node_as_str(document_context.contents) .unwrap_or_default(), node_type = document_context.node.node_type() ); diff --git a/crates/ark/src/lsp/completions/sources/composite.rs b/crates/ark/src/lsp/completions/sources/composite.rs index 369bc88317..016e39965f 100644 --- a/crates/ark/src/lsp/completions/sources/composite.rs +++ b/crates/ark/src/lsp/completions/sources/composite.rs @@ -220,8 +220,7 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::composite::get_completions; use crate::lsp::completions::sources::composite::is_identifier_like; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; use crate::r_task; use crate::treesitter::NodeType; @@ -235,8 +234,8 @@ mod tests { // identifiers that we provide completions for for keyword in ["if", "for", "while"] { let (text, point) = point_from_cursor(&format!("{keyword}@")); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert!(is_identifier_like(context.node)); assert_eq!( @@ -251,8 +250,8 @@ mod tests { fn test_get_completions_on_empty_document() { r_task(|| { let (text, point) = point_from_cursor("@"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); @@ -269,8 +268,8 @@ mod tests { r_task(|| { let code = "x <- 1:3\n@\nrnorm(3)"; let (text, point) = point_from_cursor(code); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); diff --git a/crates/ark/src/lsp/completions/sources/composite/call.rs b/crates/ark/src/lsp/completions/sources/composite/call.rs index bbec784cd9..2f355cd81b 100644 --- a/crates/ark/src/lsp/completions/sources/composite/call.rs +++ b/crates/ark/src/lsp/completions/sources/composite/call.rs @@ -76,7 +76,7 @@ fn completions_from_call( return Ok(None); }; - let callee = callee.node_as_str(&document_context.document.contents)?; + let callee = callee.node_as_str(document_context.contents)?; // - Prefer `root` as the first argument if it exists // - Then fall back to looking it up, if possible @@ -122,7 +122,7 @@ fn get_first_argument(context: &DocumentContext, node: &Node) -> anyhow::Result< return Ok(None); }; - let text = value.node_as_str(&context.document.contents)?; + let text = value.node_as_str(context.contents)?; let options = RParseEvalOptions { forbid_function_calls: true, @@ -280,8 +280,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::composite::call::completions_from_call; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; use crate::r_task; @@ -290,8 +289,8 @@ mod tests { r_task(|| { // Right after `tab` let (text, point) = point_from_cursor("match(tab@)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -303,8 +302,8 @@ mod tests { // Right after `tab` let (text, point) = point_from_cursor("match(1, tab@)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -323,8 +322,8 @@ mod tests { r_task(|| { // Place cursor between `()` let (text, point) = point_from_cursor("not_a_known_function(@)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap(); @@ -343,8 +342,8 @@ mod tests { // Place cursor between `()` let (text, point) = point_from_cursor("my_fun(@)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -360,8 +359,8 @@ mod tests { // Place just before the `()` let (text, point) = point_from_cursor("my_fun@()"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap(); @@ -369,8 +368,8 @@ mod tests { // Place just after the `()` let (text, point) = point_from_cursor("my_fun()@"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap(); @@ -392,8 +391,8 @@ mod tests { // Place cursor between `()` let (text, point) = point_from_cursor("my_fun(@)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -409,8 +408,8 @@ mod tests { r_task(|| { // No arguments typed yet let (text, point) = point_from_cursor("match(\n @\n)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -421,8 +420,8 @@ mod tests { // Partially typed argument let (text, point) = point_from_cursor("match(\n tab@\n)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -433,8 +432,8 @@ mod tests { // Partially typed second argument let (text, point) = point_from_cursor("match(\n 1,\n tab@\n)"); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap().unwrap(); @@ -450,8 +449,8 @@ mod tests { r_task(|| { fn assert_no_call_completions(code_with_cursor: &str) { let (text, point) = point_from_cursor(code_with_cursor); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_call(&context).unwrap(); diff --git a/crates/ark/src/lsp/completions/sources/composite/document.rs b/crates/ark/src/lsp/completions/sources/composite/document.rs index 94309912e9..a101af48ca 100644 --- a/crates/ark/src/lsp/completions/sources/composite/document.rs +++ b/crates/ark/src/lsp/completions/sources/composite/document.rs @@ -168,7 +168,7 @@ fn completions_from_document_function_arguments( continue; } - let parameter = node.node_as_str(&context.document.contents)?.to_string(); + let parameter = node.node_as_str(context.contents)?.to_string(); match completion_item_from_scope_parameter(parameter.as_str(), context) { Ok(item) => completions.push(item), Err(err) => log::error!("{err:?}"), @@ -185,7 +185,7 @@ fn call_uses_nse(node: &Node, context: &DocumentContext) -> bool { lhs.is_identifier_or_string().into_result()?; let value = lhs - .node_as_str(&context.document.contents)? + .node_as_str(context.contents)? .to_string(); matches!(value.as_str(), "expression" | "local" | "quote" | "enquote" | "substitute" | "with" | "within").into_result()?; diff --git a/crates/ark/src/lsp/completions/sources/composite/pipe.rs b/crates/ark/src/lsp/completions/sources/composite/pipe.rs index efe3449cda..15b0d1b0d2 100644 --- a/crates/ark/src/lsp/completions/sources/composite/pipe.rs +++ b/crates/ark/src/lsp/completions/sources/composite/pipe.rs @@ -121,7 +121,7 @@ fn find_pipe_root_name(context: &DocumentContext, node: &Node) -> anyhow::Result let Some(root) = find_pipe_root_node(context, *node)? else { return Ok(None); }; - if !root.is_pipe_operator(&context.document.contents)? { + if !root.is_pipe_operator(context.contents)? { return Ok(None); } @@ -130,7 +130,7 @@ fn find_pipe_root_name(context: &DocumentContext, node: &Node) -> anyhow::Result return Ok(None); }; - while lhs.is_pipe_operator(&context.document.contents)? { + while lhs.is_pipe_operator(context.contents)? { lhs = match lhs.child_by_field_name("lhs") { Some(lhs) => lhs, None => return Ok(None), @@ -138,7 +138,7 @@ fn find_pipe_root_name(context: &DocumentContext, node: &Node) -> anyhow::Result } // Try to evaluate the left-hand side - let root = lhs.node_as_str(&context.document.contents)?.to_string(); + let root = lhs.node_as_str(context.contents)?.to_string(); Ok(Some(root)) } @@ -150,7 +150,7 @@ fn find_pipe_root_node<'a>( let mut root = None; loop { - if node.is_pipe_operator(&context.document.contents)? { + if node.is_pipe_operator(context.contents)? { root = Some(node); } @@ -167,8 +167,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::composite::pipe::find_pipe_root; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; use crate::treesitter::node_find_containing_call; @@ -177,8 +176,8 @@ mod tests { r_task(|| { // Place cursor between `()` of `bar()` let (text, point) = point_from_cursor("x |> foo() %>% bar(@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let call_node = node_find_containing_call(context.node); let root = find_pipe_root(&context, call_node).unwrap().unwrap(); @@ -190,8 +189,8 @@ mod tests { // `%||%` is not a pipe! // Place cursor between `()` of `bar()` let (text, point) = point_from_cursor("x |> foo() %||% bar(@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let call_node = node_find_containing_call(context.node); let root = find_pipe_root(&context, call_node).unwrap(); @@ -209,8 +208,8 @@ mod tests { // Place cursor between `()` let (text, point) = point_from_cursor("x %>% foo(@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let call_node = node_find_containing_call(context.node); let root = find_pipe_root(&context, call_node).unwrap().unwrap(); diff --git a/crates/ark/src/lsp/completions/sources/composite/subset.rs b/crates/ark/src/lsp/completions/sources/composite/subset.rs index 0f9840012f..bfe9ad5939 100644 --- a/crates/ark/src/lsp/completions/sources/composite/subset.rs +++ b/crates/ark/src/lsp/completions/sources/composite/subset.rs @@ -80,7 +80,7 @@ pub(crate) fn completions_from_subset( return Ok(Some(vec![])); }; - let text = child.node_as_str(&context.document.contents)?.to_string(); + let text = child.node_as_str(context.contents)?.to_string(); completions_from_evaluated_object_names(&text, ENQUOTE, node.node_type()) } @@ -92,8 +92,7 @@ mod tests { use crate::fixtures::package_is_installed; use crate::lsp::completions::sources::composite::subset::completions_from_subset; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; #[test] @@ -109,8 +108,8 @@ mod tests { // Right after the `[` let point = Point { row: 0, column: 4 }; - let document = Document::new("foo[]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("foo[]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap().unwrap(); assert_eq!(completions.len(), 2); @@ -125,15 +124,15 @@ mod tests { // Right before the `[` let point = Point { row: 0, column: 3 }; - let document = Document::new("foo[]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("foo[]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap(); assert!(completions.is_none()); // Right after the `]` let point = Point { row: 0, column: 5 }; - let document = Document::new("foo[]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("foo[]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap(); assert!(completions.is_none()); @@ -153,8 +152,8 @@ mod tests { // Works for single comlumn name completion let point = Point { row: 0, column: 2 }; - let document = Document::new("x[]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("x[]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap().unwrap(); assert_eq!(completions.len(), 11); @@ -165,8 +164,8 @@ mod tests { // Works when completing inside a `c` call let point = Point { row: 0, column: 6 }; - let document = Document::new("x[, c()]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("x[, c()]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap().unwrap(); assert_eq!(completions.len(), 11); @@ -177,8 +176,8 @@ mod tests { // Works when completing inside a `c` call with a collumn already selected let point = Point { row: 0, column: 10 }; - let document = Document::new("x[, c(mpg,)]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("x[, c(mpg,)]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap().unwrap(); assert_eq!(completions.len(), 11); @@ -190,8 +189,8 @@ mod tests { // Works completing subset2 let point = Point { row: 0, column: 3 }; - let document = Document::new("x[[]]", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("x[[]]"); + let context = doc.context(point); let completions = completions_from_subset(&context).unwrap().unwrap(); assert_eq!(completions.len(), 11); diff --git a/crates/ark/src/lsp/completions/sources/composite/workspace.rs b/crates/ark/src/lsp/completions/sources/composite/workspace.rs index 3d677da47f..0c63740bc0 100644 --- a/crates/ark/src/lsp/completions/sources/composite/workspace.rs +++ b/crates/ark/src/lsp/completions/sources/composite/workspace.rs @@ -62,7 +62,7 @@ fn completions_from_workspace( let mut completions = vec![]; let token = if node.is_identifier() { - node.node_as_str(&context.document.contents)?.to_string() + node.node_as_str(context.contents)?.to_string() } else { "".to_string() }; diff --git a/crates/ark/src/lsp/completions/sources/unique/colon.rs b/crates/ark/src/lsp/completions/sources/unique/colon.rs index 5cf7e67948..8db3316a99 100644 --- a/crates/ark/src/lsp/completions/sources/unique/colon.rs +++ b/crates/ark/src/lsp/completions/sources/unique/colon.rs @@ -43,7 +43,7 @@ fn completions_from_single_colon( } fn is_single_colon(context: &DocumentContext) -> bool { - let Ok(text) = context.node.node_as_str(&context.document.contents) else { + let Ok(text) = context.node.node_as_str(context.contents) else { return false; }; text.eq(":") diff --git a/crates/ark/src/lsp/completions/sources/unique/comment.rs b/crates/ark/src/lsp/completions/sources/unique/comment.rs index 6437c97925..4db1915789 100644 --- a/crates/ark/src/lsp/completions/sources/unique/comment.rs +++ b/crates/ark/src/lsp/completions/sources/unique/comment.rs @@ -22,6 +22,8 @@ use crate::lsp::completions::completion_item::completion_item; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::completions::types::CompletionData; use crate::lsp::document_context::DocumentContext; +#[cfg(test)] +use crate::lsp::document_context::TestDocument; use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeTypeExt; @@ -51,7 +53,7 @@ fn completions_from_comment( let pattern = Regex::new(r"^.*\s")?; - let contents = node.node_as_str(&context.document.contents)?; + let contents = node.node_as_str(context.contents)?; let token = pattern.replace(contents, ""); let mut completions: Vec = vec![]; @@ -142,21 +144,20 @@ fn inject_roxygen_comment_after_newline(x: &str) -> String { fn test_comment() { use tree_sitter::Point; - use crate::lsp::document::Document; use crate::r_task; r_task(|| { // If not in a comment, return `None` let point = Point { row: 0, column: 1 }; - let document = Document::new("mean()", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("mean()"); + let context = doc.context(point); let completions = completions_from_comment(&context).unwrap(); assert!(completions.is_none()); // If in a comment, return empty vector let point = Point { row: 0, column: 1 }; - let document = Document::new("# mean", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("# mean"); + let context = doc.context(point); let completions = completions_from_comment(&context).unwrap().unwrap(); assert!(completions.is_empty()); }); @@ -167,7 +168,6 @@ fn test_roxygen_comment() { use libr::LOGICAL_ELT; use tree_sitter::Point; - use crate::lsp::document::Document; use crate::r_task; r_task(|| unsafe { @@ -183,8 +183,8 @@ fn test_roxygen_comment() { } let point = Point { row: 0, column: 4 }; - let document = Document::new("#' @", None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new("#' @"); + let context = doc.context(point); let completions = completions_from_comment(&context).unwrap().unwrap(); // Make sure we find it diff --git a/crates/ark/src/lsp/completions/sources/unique/custom.rs b/crates/ark/src/lsp/completions/sources/unique/custom.rs index 1d1df66e43..4bb2bde44e 100644 --- a/crates/ark/src/lsp/completions/sources/unique/custom.rs +++ b/crates/ark/src/lsp/completions/sources/unique/custom.rs @@ -210,8 +210,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::unique::custom::completions_from_custom_source; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; use crate::r_task; @@ -219,8 +218,8 @@ mod tests { fn assert_has_completion(code_with_cursor: &str, name: &str, expected_insert_text: &str) { let (text, point) = point_from_cursor(code_with_cursor); let state = WorldState::default(); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_custom_source(&context).unwrap().unwrap(); @@ -237,8 +236,8 @@ mod tests { fn assert_no_completions(code_with_cursor: &str) { let (text, point) = point_from_cursor(code_with_cursor); let state = WorldState::default(); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let context = CompletionContext::new(&document_context, &state); let completions = completions_from_custom_source(&context).unwrap(); @@ -256,8 +255,8 @@ mod tests { let (text, point) = point_from_cursor("library(@)"); let state = WorldState::default(); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let context = CompletionContext::new(&document_context, &state); let n_compls = completions_from_custom_source(&context) @@ -270,8 +269,8 @@ mod tests { let (text, point) = point_from_cursor("library(uti@)"); let state = WorldState::default(); - let document = Document::new(text.as_str(), None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let context = CompletionContext::new(&document_context, &state); let compls = completions_from_custom_source(&context).unwrap().unwrap(); diff --git a/crates/ark/src/lsp/completions/sources/unique/extractor.rs b/crates/ark/src/lsp/completions/sources/unique/extractor.rs index c9682b4b75..c6cfa74ff0 100644 --- a/crates/ark/src/lsp/completions/sources/unique/extractor.rs +++ b/crates/ark/src/lsp/completions/sources/unique/extractor.rs @@ -100,7 +100,7 @@ fn completions_from_extractor( }; // Extract out its name from the document - let text = node.node_as_str(&context.document.contents)?; + let text = node.node_as_str(context.contents)?; completions.append(&mut completions_from_extractor_object(text, fun)?); @@ -208,8 +208,7 @@ mod tests { use crate::fixtures::package_is_installed; use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::extractor::completions_from_dollar; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; #[test] @@ -224,8 +223,8 @@ mod tests { harp::parse_eval("foo <- list(b = 1, a = 2)", options.clone()).unwrap(); let (text, point) = point_from_cursor("foo$@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let completions = completions_from_dollar(&context).unwrap().unwrap(); assert_eq!(completions.len(), 2); @@ -238,8 +237,8 @@ mod tests { assert_eq!(completion.label, "a".to_string()); let (text, point) = point_from_cursor("foo@$"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let completions = completions_from_dollar(&context).unwrap(); assert!(completions.is_none()); @@ -261,8 +260,8 @@ mod tests { assert_eq!(r_lgl_get(exists.sexp, 0), 0); let (text, point) = point_from_cursor("foo$@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // No error and empty completions list // (If the user is typing pseudocode, we want to respect that and say that we @@ -272,8 +271,8 @@ mod tests { assert_eq!(completions.len(), 0); let (text, point) = point_from_cursor("foo$mat@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // Same as above let completions = completions_from_dollar(&context).unwrap().unwrap(); @@ -285,8 +284,8 @@ mod tests { fn test_dollar_completions_on_complex_lhs() { r_task(|| { let (text, point) = point_from_cursor("list(a = 1, b = 2)$@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // No error and empty completions list // We know we are on the RHS of a `$`, but `r_parse_eval()` will fail on the @@ -310,8 +309,8 @@ mod tests { harp::parse_eval("foo <- list(b = 1, a = 2)", options.clone()).unwrap(); let (text, point) = point_from_cursor("foo@$"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // `None` because we have no completions to provide, and we do want other // completion sources to get a chance to run, as you can put arbitrary @@ -336,8 +335,8 @@ mod tests { harp::parse_eval("foo <- list(abcd = 1, wxyz = 2)", options.clone()).unwrap(); let (text, point) = point_from_cursor("foo$abc@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // All names of `foo`, the frontend filters them let completions = completions_from_dollar(&context).unwrap().unwrap(); @@ -346,8 +345,8 @@ mod tests { assert_eq!(completions.get(1).unwrap().label, String::from("wxyz")); let (text, point) = point_from_cursor("foo$a@bc"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // Same as above let completions = completions_from_dollar(&context).unwrap().unwrap(); @@ -383,8 +382,8 @@ foo <- Foo$new() .unwrap(); let (text, point) = point_from_cursor("foo$@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // We get some default R6 methods back, but we are looking for `abc` let completions = completions_from_dollar(&context).unwrap().unwrap(); diff --git a/crates/ark/src/lsp/completions/sources/unique/file_path.rs b/crates/ark/src/lsp/completions/sources/unique/file_path.rs index 60a9a1b473..fc6f250c74 100644 --- a/crates/ark/src/lsp/completions/sources/unique/file_path.rs +++ b/crates/ark/src/lsp/completions/sources/unique/file_path.rs @@ -32,7 +32,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 // by parsing it before searching the path entries. - let token = node.node_as_str(&context.document.contents)?; + let token = node.node_as_str(context.contents)?; // It's entirely possible that we can fail to parse the string, `R_ParseVector()` // can fail in various ways. We silently swallow these because they are unlikely @@ -96,8 +96,7 @@ pub(super) fn completions_from_string_file_path( mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::file_path::completions_from_string_file_path; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; use crate::treesitter::node_find_string; @@ -107,8 +106,8 @@ mod tests { r_task(|| { // "\R" is an unrecognized escape character and `R_ParseVector()` errors on it let (text, point) = point_from_cursor(r#" ".\R\utils.R@" "#); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_file_path(&node, &context).unwrap(); diff --git a/crates/ark/src/lsp/completions/sources/unique/namespace.rs b/crates/ark/src/lsp/completions/sources/unique/namespace.rs index c23e48d305..f89f0ff54b 100644 --- a/crates/ark/src/lsp/completions/sources/unique/namespace.rs +++ b/crates/ark/src/lsp/completions/sources/unique/namespace.rs @@ -76,7 +76,7 @@ fn completions_from_namespace( return Ok(Some(completions)); }; - let package = package.node_as_str(&context.document.contents)?; + let package = package.node_as_str(context.contents)?; // Get the package namespace let Ok(namespace) = RFunction::new("base", "getNamespace").add(package).call() else { @@ -246,8 +246,7 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::unique::namespace::completions_from_namespace; use crate::lsp::completions::tests::utils::find_completion_by_label; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; use crate::r_task; @@ -255,8 +254,8 @@ mod tests { cursor_text: &str, ) -> anyhow::Result>> { let (text, point) = point_from_cursor(cursor_text); - let document = Document::new(&text, None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); let context = CompletionContext::new(&document_context, &state); diff --git a/crates/ark/src/lsp/completions/sources/unique/string.rs b/crates/ark/src/lsp/completions/sources/unique/string.rs index 1e0244dd40..9f19bd2c32 100644 --- a/crates/ark/src/lsp/completions/sources/unique/string.rs +++ b/crates/ark/src/lsp/completions/sources/unique/string.rs @@ -77,8 +77,7 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::unique; use crate::lsp::completions::sources::unique::string::completions_from_string; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; use crate::r_task; use crate::treesitter::node_find_string; @@ -90,8 +89,8 @@ mod tests { // Before or after the `''`, i.e. `|''` or `''|`. // Still considered part of the string node. let (text, point) = point_from_cursor("@''"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert!(node_find_string(&context.node).is_some()); assert_eq!(completions_from_string(&context).unwrap(), None); @@ -102,8 +101,8 @@ mod tests { fn test_not_string() { r_task(|| { let (text, point) = point_from_cursor("@foo"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert!(context.node.is_identifier()); assert_eq!(completions_from_string(&context).unwrap(), None); @@ -116,10 +115,10 @@ mod tests { let (text, point) = point_from_cursor("'~/@'"); // Assume home directory is not empty - let document = Document::new(text.as_str(), None); + let doc = TestDocument::new(&text); // `None` trigger -> Return file completions - let context = DocumentContext::new(&document, point, None); + let context = doc.context(point); assert_match!( completions_from_string(&context).unwrap(), Some(items) => { @@ -128,7 +127,7 @@ mod tests { ); // `Some` trigger -> Should return empty completion set - let context = DocumentContext::new(&document, point, Some(String::from("$"))); + let context = doc.context_with_trigger(point, Some(String::from("$"))); let res = completions_from_string(&context).unwrap(); assert_match!(res, Some(items) => { assert!(items.is_empty()) }); diff --git a/crates/ark/src/lsp/completions/sources/unique/subset.rs b/crates/ark/src/lsp/completions/sources/unique/subset.rs index be83a25c78..921749bc33 100644 --- a/crates/ark/src/lsp/completions/sources/unique/subset.rs +++ b/crates/ark/src/lsp/completions/sources/unique/subset.rs @@ -46,7 +46,7 @@ pub(super) fn completions_from_string_subset( // completion sources from running. let mut completions: Vec = vec![]; - let text = node.node_as_str(&context.document.contents)?; + let text = node.node_as_str(context.contents)?; if let Some(mut candidates) = completions_from_evaluated_object_names(text, ENQUOTE, node.node_type())? @@ -68,7 +68,7 @@ fn node_find_object_for_string_subset<'tree>( let mut node = node_find_parent_call(node)?; if node.is_call() { - if !node_is_c_call(&node, &context.document.contents) { + if !node_is_c_call(&node, context.contents) { // Inside a call that isn't `c()` return None; } @@ -124,8 +124,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::subset::completions_from_string_subset; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; use crate::treesitter::node_find_string; @@ -137,8 +136,8 @@ mod tests { // Inside top level `""` let (text, point) = point_from_cursor(r#"foo["@"]"#); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) @@ -158,8 +157,8 @@ mod tests { // Inside `""` in `[[` let (text, point) = point_from_cursor(r#"foo[["@"]]"#); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) .unwrap() @@ -168,8 +167,8 @@ mod tests { // 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 doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) .unwrap() @@ -178,8 +177,8 @@ mod tests { // 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 doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) .unwrap() @@ -188,8 +187,8 @@ mod tests { // 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 doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) .unwrap() @@ -200,8 +199,8 @@ mod tests { // Instead file path completions should kick in, because this is an arbitrary // function call so subset completions don't make sense, but file ones might. 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 doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context).unwrap(); assert!(completions.is_none()); @@ -209,8 +208,8 @@ mod tests { // 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 doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) .unwrap() @@ -230,8 +229,8 @@ mod tests { parse_eval_global("colnames(foo) <- c('a', 'b')").unwrap(); let (text, point) = point_from_cursor(r#"foo[, "@"]"#); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); let node = node_find_string(&context.node).unwrap(); let completions = completions_from_string_subset(&node, &context) diff --git a/crates/ark/src/lsp/completions/sources/utils.rs b/crates/ark/src/lsp/completions/sources/utils.rs index ed2ab79198..2be22ed892 100644 --- a/crates/ark/src/lsp/completions/sources/utils.rs +++ b/crates/ark/src/lsp/completions/sources/utils.rs @@ -100,7 +100,7 @@ pub(super) fn filter_out_dot_prefixes( // Remove completions that start with `.` unless the user explicitly requested them let user_requested_dot = context .node - .node_as_str(&context.document.contents) + .node_as_str(context.contents) .map(|x| x.starts_with(".")) .unwrap_or(false); @@ -293,8 +293,7 @@ mod tests { use crate::lsp::completions::sources::utils::call_node_position_type; use crate::lsp::completions::sources::utils::completions_from_evaluated_object_names; use crate::lsp::completions::sources::utils::CallNodePositionType; - use crate::lsp::document::Document; - use crate::lsp::document_context::DocumentContext; + use crate::lsp::document_context::TestDocument; use crate::r_task; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -303,8 +302,8 @@ mod tests { fn test_call_node_position_type() { // Before `(`, but on it let (text, point) = point_from_cursor("fn @()"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from("(")) @@ -316,8 +315,8 @@ mod tests { // After `)`, but on it let (text, point) = point_from_cursor("fn()@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from(")")) @@ -329,8 +328,8 @@ mod tests { // After `(`, but on it let (text, point) = point_from_cursor("fn(@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from("(")) @@ -342,8 +341,8 @@ mod tests { // After `x` let (text, point) = point_from_cursor("fn(x@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( call_node_position_type(&context.node, context.point), CallNodePositionType::Ambiguous @@ -351,8 +350,8 @@ mod tests { // After `x` let (text, point) = point_from_cursor("fn(1, x@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( call_node_position_type(&context.node, context.point), CallNodePositionType::Ambiguous @@ -360,8 +359,8 @@ mod tests { // Directly after `,` let (text, point) = point_from_cursor("fn(x,@ )"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Comma); assert_eq!( call_node_position_type(&context.node, context.point), @@ -370,8 +369,8 @@ mod tests { // After `,`, but on `)` let (text, point) = point_from_cursor("fn(x, @)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from(")")) @@ -383,8 +382,8 @@ mod tests { // After `=` let (text, point) = point_from_cursor("fn(x =@ )"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from("=")) @@ -396,8 +395,8 @@ mod tests { // In an expression let (text, point) = point_from_cursor("fn(1@ + 1)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Float); assert_eq!( call_node_position_type(&context.node, context.point), @@ -405,8 +404,8 @@ mod tests { ); let (text, point) = point_from_cursor("fn(1 + 1@)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Float); assert_eq!( call_node_position_type(&context.node, context.point), @@ -416,8 +415,8 @@ mod tests { // Right before an expression // (special case where we still provide argument completions) let (text, point) = point_from_cursor("fn(1, @1 + 1)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Float); assert_eq!( call_node_position_type(&context.node, context.point), @@ -427,8 +426,8 @@ mod tests { // After an identifier, before the `)`, with whitespace between them, // but on the `)` let (text, point) = point_from_cursor("fn(x @)"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!( context.node.node_type(), NodeType::Anonymous(String::from(")")) @@ -441,8 +440,8 @@ mod tests { // After an identifier, before the `)`, with whitespace between them, // but on the identifier let (text, point) = point_from_cursor("fn(x@ )"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert!(context.node.is_identifier()); assert_eq!( call_node_position_type(&context.node, context.point), @@ -451,8 +450,8 @@ mod tests { // After `(`, and on own line let (text, point) = point_from_cursor("fn(\n @\n)"); - let document = Document::new(&text, None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Arguments); assert_eq!( diff --git a/crates/ark/src/lsp/completions/tests/utils.rs b/crates/ark/src/lsp/completions/tests/utils.rs index e9ffa26b48..b09a12de1f 100644 --- a/crates/ark/src/lsp/completions/tests/utils.rs +++ b/crates/ark/src/lsp/completions/tests/utils.rs @@ -11,14 +11,13 @@ use tower_lsp::lsp_types::CompletionTextEdit; use crate::fixtures::point_from_cursor; use crate::lsp::completions::provide_completions; use crate::lsp::completions::sources::utils::has_priority_prefix; -use crate::lsp::document::Document; -use crate::lsp::document_context::DocumentContext; +use crate::lsp::document_context::TestDocument; use crate::lsp::state::WorldState; pub(crate) fn get_completions_at_cursor(cursor_text: &str) -> anyhow::Result> { let (text, point) = point_from_cursor(cursor_text); - let document = Document::new(&text, None); - let document_context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let document_context = doc.context(point); let state = WorldState::default(); match provide_completions(&document_context, &state) { diff --git a/crates/ark/src/lsp/document_context.rs b/crates/ark/src/lsp/document_context.rs index 3e622cd911..e6d3e39001 100644 --- a/crates/ark/src/lsp/document_context.rs +++ b/crates/ark/src/lsp/document_context.rs @@ -5,17 +5,26 @@ // // +use aether_lsp_utils::proto::PositionEncoding; use tree_sitter::Node; use tree_sitter::Point; -use crate::lsp::document::Document; +#[cfg(test)] +use crate::lsp::ark_file::test_ark_file; +#[cfg(test)] +use crate::lsp::ark_file::ArkFile; use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; #[derive(Debug)] pub struct DocumentContext<'a> { - pub document: &'a Document, + /// We store extracted components of `&ArkFile` + `&dyn ArkDb` here because + /// the latter can't be sent over an `r_task()`. + pub tree: &'a tree_sitter::Tree, + pub contents: &'a str, + pub line_index: &'a biome_line_index::LineIndex, + pub encoding: PositionEncoding, pub node: Node<'a>, /// Formerly known just as "node". This renaming unblocks completion /// improvements, where we really do want to focus on the smallest node @@ -28,22 +37,24 @@ pub struct DocumentContext<'a> { } impl<'a> DocumentContext<'a> { - pub fn new(document: &'a Document, point: Point, trigger: Option) -> Self { - // get reference to AST - let ast = &document.ast; - - let Some(node) = ast.root_node().find_smallest_spanning_node(point) else { - let contents = document.contents.to_string(); + pub fn new( + tree: &'a tree_sitter::Tree, + contents: &'a str, + line_index: &'a biome_line_index::LineIndex, + encoding: PositionEncoding, + point: Point, + trigger: Option, + ) -> Self { + let Some(node) = tree.root_node().find_smallest_spanning_node(point) else { panic!( "Failed to find spanning node containing point: {point} with contents '{contents}'" ); }; // find closest node at point - let Some(closest_node) = ast.root_node().find_closest_node_to_point(point) else { + let Some(closest_node) = tree.root_node().find_closest_node_to_point(point) else { // TODO: We really want to track this down and figure out what's happening // and fix it in `find_closest_node_to_point()`. - let contents = document.contents.to_string(); panic!("Failed to find closest node to point: {point} with contents '{contents}'"); }; @@ -85,7 +96,10 @@ impl<'a> DocumentContext<'a> { }; DocumentContext { - document, + tree, + contents, + line_index, + encoding, node, closest_node, point, @@ -94,6 +108,41 @@ impl<'a> DocumentContext<'a> { } } +/// Owns a `db` + `ArkFile` so unit tests can build a `DocumentContext` the same +/// way handlers do, borrowing the cached tree and line index from the database. +#[cfg(test)] +pub(crate) struct TestDocument { + db: oak_db::OakDatabase, + file: ArkFile, +} + +#[cfg(test)] +impl TestDocument { + pub(crate) fn new(contents: &str) -> Self { + let (db, file) = test_ark_file(contents); + Self { db, file } + } + + pub(crate) fn context(&self, point: Point) -> DocumentContext<'_> { + self.context_with_trigger(point, None) + } + + pub(crate) fn context_with_trigger( + &self, + point: Point, + trigger: Option, + ) -> DocumentContext<'_> { + DocumentContext::new( + self.file.tree_sitter(&self.db), + self.file.contents(&self.db), + self.file.line_index(&self.db), + self.file.encoding, + point, + trigger, + ) + } +} + #[cfg(test)] mod tests { use super::*; @@ -105,27 +154,15 @@ mod tests { fn test_document_context_start_of_document() { // Empty document let (text, point) = point_from_cursor("@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); - assert_eq!( - context - .node - .node_as_str(&context.document.contents) - .unwrap(), - "" - ); + let doc = TestDocument::new(&text); + let context = doc.context(point); + assert_eq!(context.node.node_as_str(context.contents).unwrap(), ""); // Start of document with text let (text, point) = point_from_cursor("@1 + 1"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); - assert_eq!( - context - .node - .node_as_str(&context.document.contents) - .unwrap(), - "1" - ); + let doc = TestDocument::new(&text); + let context = doc.context(point); + assert_eq!(context.node.node_as_str(context.contents).unwrap(), "1"); } #[test] @@ -133,31 +170,22 @@ mod tests { // Cursor at end of identifier "lib" at position (0, 3) // This reproduced a panic where find_smallest_spanning_node returned None let (text, point) = point_from_cursor("lib@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); // The node should be the identifier "lib" - assert_eq!( - context - .node - .node_as_str(&context.document.contents) - .unwrap(), - "lib" - ); + assert_eq!(context.node.node_as_str(context.contents).unwrap(), "lib"); } #[test] fn test_document_context_cursor_on_empty_line() { // as if we're about to type on the second line let (text, point) = point_from_cursor("toupper(letters)\n@"); - let document = Document::new(text.as_str(), None); - let context = DocumentContext::new(&document, point, None); + let doc = TestDocument::new(&text); + let context = doc.context(point); assert_eq!(context.node.node_type(), NodeType::Program); assert_eq!( - context - .node - .node_as_str(&context.document.contents) - .unwrap(), + context.node.node_as_str(context.contents).unwrap(), "toupper(letters)\n" ); @@ -166,10 +194,7 @@ mod tests { NodeType::Anonymous(String::from(")")) ); assert_eq!( - context - .closest_node - .node_as_str(&context.document.contents) - .unwrap(), + context.closest_node.node_as_str(context.contents).unwrap(), ")" ); } diff --git a/crates/ark/src/lsp/handlers.rs b/crates/ark/src/lsp/handlers.rs index 470180a006..a79178341f 100644 --- a/crates/ark/src/lsp/handlers.rs +++ b/crates/ark/src/lsp/handlers.rs @@ -5,7 +5,6 @@ // // -use aether_path::FilePath; use anyhow::anyhow; use serde_json::Value; use stdext::result::ResultExt; @@ -210,17 +209,24 @@ pub(crate) fn handle_completion( params: CompletionParams, state: &WorldState, ) -> LspResult> { - // Get reference to document. let uri = params.text_document_position.text_document.uri; - let document = state.get_document(&FilePath::from_url(&uri))?; + let file = state.ark_file(&uri)?; + let db = &state.db; + let encoding = state.config.position_encoding; let position = params.text_document_position.position; - let point = document.tree_sitter_point_from_lsp_position(position)?; + let point = file.tree_sitter_point_from_lsp_position(db, position)?; let trigger = params.context.and_then(|ctxt| ctxt.trigger_character); - // Build the document context. - let context = DocumentContext::new(document, point, trigger); + let context = DocumentContext::new( + file.tree_sitter(db), + file.contents(db), + file.line_index(db), + encoding, + point, + trigger, + ); lsp::log_info!("Completion context: {:#?}", context); // TODO(oak/completions): Clone so the closure captures by value. `r_task()` @@ -245,13 +251,21 @@ pub(crate) fn handle_completion_resolve(mut item: CompletionItem) -> LspResult LspResult> { let uri = params.text_document_position_params.text_document.uri; - let document = state.get_document(&FilePath::from_url(&uri))?; + let file = state.ark_file(&uri)?; + let db = &state.db; + let encoding = state.config.position_encoding; let position = params.text_document_position_params.position; - let point = document.tree_sitter_point_from_lsp_position(position)?; + let point = file.tree_sitter_point_from_lsp_position(db, position)?; - // build document context - let context = DocumentContext::new(document, point, None); + let context = DocumentContext::new( + file.tree_sitter(db), + file.contents(db), + file.line_index(db), + encoding, + point, + None, + ); // request hover information let result = r_task(|| r_hover(&context)); @@ -280,12 +294,21 @@ pub(crate) fn handle_signature_help( state: &WorldState, ) -> LspResult> { let uri = params.text_document_position_params.text_document.uri; - let document = state.get_document(&FilePath::from_url(&uri))?; + let file = state.ark_file(&uri)?; + let db = &state.db; + let encoding = state.config.position_encoding; let position = params.text_document_position_params.position; - let point = document.tree_sitter_point_from_lsp_position(position)?; - - let context = DocumentContext::new(document, point, None); + let point = file.tree_sitter_point_from_lsp_position(db, position)?; + + let context = DocumentContext::new( + file.tree_sitter(db), + file.contents(db), + file.line_index(db), + encoding, + point, + None, + ); // request signature help let result = r_task(|| r_signature_help(&context)); diff --git a/crates/ark/src/lsp/hover.rs b/crates/ark/src/lsp/hover.rs index 5dadb1f29e..15ee8181b5 100644 --- a/crates/ark/src/lsp/hover.rs +++ b/crates/ark/src/lsp/hover.rs @@ -43,8 +43,8 @@ fn hover_context(node: Node, context: &DocumentContext) -> Result Result anyhow::Result> { // Get document AST + completion position. - let ast = &context.document.ast; + let ast = &context.tree; // Find the node closest to the completion point. let node = ast.root_node(); @@ -106,7 +106,7 @@ pub(crate) fn r_signature_help(context: &DocumentContext) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result