Skip to content

Commit e6a54a1

Browse files
authored
Use smallest spanning node as the "starter" node in completions (#805)
* Use smallest spanning node as the "starter" node in completions I suspect there's broader refactoring that would make sense in node-finding in areas like signature help, hover, etc., but I don't want to open that can of worms at this time. * Use node_text() * Use point_from_cursor() in these tests * Adding a (currently failing) test for scenario identified by @DavisVaughan * Anticipate being in an "arguments" node when completing inside a call * Assert the node type * Thoroughly test multiline custom completions with Sys.getenv() * Use point_from_cursor() in these tests * Add tests for argument completion inside a multiline call * Feels like a better test name * Add (temporarily failing) tests re: "value" position * `closest_node` is better in this edge case * Add a new field in completion context for "Are we inside a call?" This logic is repeated in several places with an admonishment to keep it in sync. Let's keep in sync by definition. * Use is_in_call for custom completions And create some test helpers * Use is_in_call for call completions Generally lean more into completion context. Add tests for (lack of) completion in "value" position. * Extract into a helper * Didn't mean to retain this logging * Use containing_call_cell instead of is_in_call_call
1 parent 109e76b commit e6a54a1

File tree

8 files changed

+437
-329
lines changed

8 files changed

+437
-329
lines changed

crates/ark/src/lsp/completions/completion_context.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77

88
use std::cell::OnceCell;
99

10+
use tree_sitter::Node;
11+
1012
use crate::lsp::completions::parameter_hints::ParameterHints;
1113
use crate::lsp::completions::parameter_hints::{self};
1214
use crate::lsp::completions::sources::composite::pipe::find_pipe_root;
1315
use crate::lsp::completions::sources::composite::pipe::PipeRoot;
1416
use crate::lsp::document_context::DocumentContext;
1517
use crate::lsp::state::WorldState;
18+
use crate::treesitter::node_find_containing_call;
1619

1720
pub(crate) struct CompletionContext<'a> {
1821
pub(crate) document_context: &'a DocumentContext<'a>,
1922
pub(crate) state: &'a WorldState,
2023
parameter_hints_cell: OnceCell<ParameterHints>,
2124
pipe_root_cell: OnceCell<Option<PipeRoot>>,
25+
containing_call_cell: OnceCell<Option<Node<'a>>>,
2226
}
2327

2428
impl<'a> CompletionContext<'a> {
@@ -28,6 +32,7 @@ impl<'a> CompletionContext<'a> {
2832
state,
2933
parameter_hints_cell: OnceCell::new(),
3034
pipe_root_cell: OnceCell::new(),
35+
containing_call_cell: OnceCell::new(),
3136
}
3237
}
3338

@@ -41,14 +46,22 @@ impl<'a> CompletionContext<'a> {
4146
}
4247

4348
pub fn pipe_root(&self) -> Option<PipeRoot> {
49+
let call_node = self.containing_call_node();
50+
4451
self.pipe_root_cell
45-
.get_or_init(|| match find_pipe_root(self.document_context) {
52+
.get_or_init(|| match find_pipe_root(self.document_context, call_node) {
4653
Ok(root) => root,
4754
Err(e) => {
48-
log::error!("Error trying to find pipe root: {}", e);
55+
log::error!("Error trying to find pipe root: {e}");
4956
None
5057
},
5158
})
5259
.clone()
5360
}
61+
62+
pub fn containing_call_node(&self) -> Option<Node<'a>> {
63+
*self
64+
.containing_call_cell
65+
.get_or_init(|| node_find_containing_call(self.document_context.node))
66+
}
5467
}

crates/ark/src/lsp/completions/sources/composite/call.rs

Lines changed: 132 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use harp::utils::r_is_function;
1414
use tower_lsp::lsp_types::CompletionItem;
1515
use tree_sitter::Node;
1616

17-
use super::pipe::PipeRoot;
1817
use crate::lsp::completions::completion_context::CompletionContext;
1918
use crate::lsp::completions::completion_item::completion_item_from_parameter;
2019
use crate::lsp::completions::sources::utils::call_node_position_type;
@@ -24,7 +23,6 @@ use crate::lsp::completions::sources::CompletionSource;
2423
use crate::lsp::document_context::DocumentContext;
2524
use crate::lsp::indexer;
2625
use crate::lsp::traits::rope::RopeExt;
27-
use crate::treesitter::NodeTypeExt;
2826

2927
pub(super) struct CallSource;
3028

@@ -37,45 +35,20 @@ impl CompletionSource for CallSource {
3735
&self,
3836
completion_context: &CompletionContext,
3937
) -> anyhow::Result<Option<Vec<CompletionItem>>> {
40-
completions_from_call(
41-
completion_context.document_context,
42-
completion_context.pipe_root(),
43-
)
38+
completions_from_call(completion_context)
4439
}
4540
}
4641

4742
fn completions_from_call(
48-
context: &DocumentContext,
49-
root: Option<PipeRoot>,
43+
context: &CompletionContext,
5044
) -> anyhow::Result<Option<Vec<CompletionItem>>> {
51-
let mut node = context.node;
52-
let mut has_call = false;
53-
54-
loop {
55-
// If we landed on a 'call', then we should provide parameter completions
56-
// for the associated callee if possible.
57-
if node.is_call() {
58-
has_call = true;
59-
break;
60-
}
61-
62-
// If we reach a brace list, bail.
63-
if node.is_braced_expression() {
64-
break;
65-
}
66-
67-
// Update the node.
68-
node = match node.parent() {
69-
Some(node) => node,
70-
None => break,
71-
};
72-
}
73-
74-
if !has_call {
75-
// Didn't detect anything worth completing in this context,
76-
// let other sources add their own candidates instead
45+
let Some(node) = context.containing_call_node() else {
46+
// Not in a call, let other sources add their own candidates
7747
return Ok(None);
78-
}
48+
};
49+
50+
let document_context = context.document_context;
51+
let point = document_context.point;
7952

8053
// Now that we know we are in a call, detect if we are in a location where
8154
// we should provide argument completions, i.e. if we are in the `name`
@@ -84,7 +57,7 @@ fn completions_from_call(
8457
// fn(name = value)
8558
// ~~~~
8659
//
87-
match call_node_position_type(&context.node, context.point) {
60+
match call_node_position_type(&document_context.node, point) {
8861
// We should provide argument completions. Ambiguous states like
8962
// `fn(arg<tab>)` or `fn(x, arg<tab>)` should still get argument
9063
// completions.
@@ -102,23 +75,28 @@ fn completions_from_call(
10275
return Ok(None);
10376
};
10477

105-
let callee = context.document.contents.node_slice(&callee)?.to_string();
78+
let callee = document_context
79+
.document
80+
.contents
81+
.node_slice(&callee)?
82+
.to_string();
10683

10784
// - Prefer `root` as the first argument if it exists
10885
// - Then fall back to looking it up, if possible
10986
// - Otherwise use `NULL` to signal that we can't figure it out
87+
let root = context.pipe_root();
11088
let object = match root {
11189
Some(root) => match root.object {
11290
Some(object) => object,
11391
None => RObject::null(),
11492
},
115-
None => match get_first_argument(context, &node)? {
93+
None => match get_first_argument(document_context, &node)? {
11694
Some(object) => object,
11795
None => RObject::null(),
11896
},
11997
};
12098

121-
completions_from_arguments(context, &callee, object)
99+
completions_from_arguments(document_context, &callee, object)
122100
}
123101

124102
fn get_first_argument(context: &DocumentContext, node: &Node) -> anyhow::Result<Option<RObject>> {
@@ -302,32 +280,38 @@ fn completions_from_workspace_arguments(
302280
#[cfg(test)]
303281
mod tests {
304282
use harp::eval::RParseEvalOptions;
305-
use tree_sitter::Point;
306283

284+
use crate::fixtures::point_from_cursor;
285+
use crate::lsp::completions::completion_context::CompletionContext;
307286
use crate::lsp::completions::sources::composite::call::completions_from_call;
308287
use crate::lsp::document_context::DocumentContext;
309288
use crate::lsp::documents::Document;
289+
use crate::lsp::state::WorldState;
310290
use crate::r_task;
311291

312292
#[test]
313293
fn test_completions_after_user_types_part_of_an_argument_name() {
314294
r_task(|| {
315295
// Right after `tab`
316-
let point = Point { row: 0, column: 9 };
317-
let document = Document::new("match(tab)", None);
318-
let context = DocumentContext::new(&document, point, None);
319-
let completions = completions_from_call(&context, None).unwrap().unwrap();
296+
let (text, point) = point_from_cursor("match(tab@)");
297+
let document = Document::new(text.as_str(), None);
298+
let document_context = DocumentContext::new(&document, point, None);
299+
let state = WorldState::default();
300+
let context = CompletionContext::new(&document_context, &state);
301+
let completions = completions_from_call(&context).unwrap().unwrap();
320302

321303
// We detect this as a `name` position and return all possible completions
322304
assert_eq!(completions.len(), 4);
323305
assert_eq!(completions.get(0).unwrap().label, "x = ");
324306
assert_eq!(completions.get(1).unwrap().label, "table = ");
325307

326308
// Right after `tab`
327-
let point = Point { row: 0, column: 12 };
328-
let document = Document::new("match(1, tab)", None);
329-
let context = DocumentContext::new(&document, point, None);
330-
let completions = completions_from_call(&context, None).unwrap().unwrap();
309+
let (text, point) = point_from_cursor("match(1, tab@)");
310+
let document = Document::new(text.as_str(), None);
311+
let document_context = DocumentContext::new(&document, point, None);
312+
let state = WorldState::default();
313+
let context = CompletionContext::new(&document_context, &state);
314+
let completions = completions_from_call(&context).unwrap().unwrap();
331315

332316
// We detect this as a `name` position and return all possible completions
333317
// (TODO: Should not return `x` as a possible completion)
@@ -342,10 +326,12 @@ mod tests {
342326
// Can't find the function
343327
r_task(|| {
344328
// Place cursor between `()`
345-
let point = Point { row: 0, column: 21 };
346-
let document = Document::new("not_a_known_function()", None);
347-
let context = DocumentContext::new(&document, point, None);
348-
let completions = completions_from_call(&context, None).unwrap();
329+
let (text, point) = point_from_cursor("not_a_known_function(@)");
330+
let document = Document::new(text.as_str(), None);
331+
let document_context = DocumentContext::new(&document, point, None);
332+
let state = WorldState::default();
333+
let context = CompletionContext::new(&document_context, &state);
334+
let completions = completions_from_call(&context).unwrap();
349335
assert!(completions.is_none());
350336
});
351337

@@ -360,10 +346,12 @@ mod tests {
360346
harp::parse_eval("my_fun <- function(y, x) x + y", options.clone()).unwrap();
361347

362348
// Place cursor between `()`
363-
let point = Point { row: 0, column: 7 };
364-
let document = Document::new("my_fun()", None);
365-
let context = DocumentContext::new(&document, point, None);
366-
let completions = completions_from_call(&context, None).unwrap().unwrap();
349+
let (text, point) = point_from_cursor("my_fun(@)");
350+
let document = Document::new(text.as_str(), None);
351+
let document_context = DocumentContext::new(&document, point, None);
352+
let state = WorldState::default();
353+
let context = CompletionContext::new(&document_context, &state);
354+
let completions = completions_from_call(&context).unwrap().unwrap();
367355

368356
assert_eq!(completions.len(), 2);
369357

@@ -375,17 +363,21 @@ mod tests {
375363
assert_eq!(completion.label, "x = ");
376364

377365
// Place just before the `()`
378-
let point = Point { row: 0, column: 6 };
379-
let document = Document::new("my_fun()", None);
380-
let context = DocumentContext::new(&document, point, None);
381-
let completions = completions_from_call(&context, None).unwrap();
366+
let (text, point) = point_from_cursor("my_fun@()");
367+
let document = Document::new(text.as_str(), None);
368+
let document_context = DocumentContext::new(&document, point, None);
369+
let state = WorldState::default();
370+
let context = CompletionContext::new(&document_context, &state);
371+
let completions = completions_from_call(&context).unwrap();
382372
assert!(completions.is_none());
383373

384374
// Place just after the `()`
385-
let point = Point { row: 0, column: 8 };
386-
let document = Document::new("my_fun()", None);
387-
let context = DocumentContext::new(&document, point, None);
388-
let completions = completions_from_call(&context, None).unwrap();
375+
let (text, point) = point_from_cursor("my_fun()@");
376+
let document = Document::new(text.as_str(), None);
377+
let document_context = DocumentContext::new(&document, point, None);
378+
let state = WorldState::default();
379+
let context = CompletionContext::new(&document_context, &state);
380+
let completions = completions_from_call(&context).unwrap();
389381
assert!(completions.is_none());
390382

391383
// Clean up
@@ -403,14 +395,86 @@ mod tests {
403395
harp::parse_eval("my_fun <- 1", options.clone()).unwrap();
404396

405397
// Place cursor between `()`
406-
let point = Point { row: 0, column: 7 };
407-
let document = Document::new("my_fun()", None);
408-
let context = DocumentContext::new(&document, point, None);
409-
let completions = completions_from_call(&context, None).unwrap().unwrap();
398+
let (text, point) = point_from_cursor("my_fun(@)");
399+
let document = Document::new(text.as_str(), None);
400+
let document_context = DocumentContext::new(&document, point, None);
401+
let state = WorldState::default();
402+
let context = CompletionContext::new(&document_context, &state);
403+
let completions = completions_from_call(&context).unwrap().unwrap();
410404
assert_eq!(completions.len(), 0);
411405

412406
// Clean up
413407
harp::parse_eval("remove(my_fun)", options.clone()).unwrap();
414408
})
415409
}
410+
411+
#[test]
412+
fn test_completions_multiline_call() {
413+
r_task(|| {
414+
// No arguments typed yet
415+
let (text, point) = point_from_cursor("match(\n @\n)");
416+
let document = Document::new(text.as_str(), None);
417+
let document_context = DocumentContext::new(&document, point, None);
418+
let state = WorldState::default();
419+
let context = CompletionContext::new(&document_context, &state);
420+
let completions = completions_from_call(&context).unwrap().unwrap();
421+
422+
assert_eq!(completions.len(), 4);
423+
assert_eq!(completions.get(0).unwrap().label, "x = ");
424+
assert_eq!(completions.get(1).unwrap().label, "table = ");
425+
426+
// Partially typed argument
427+
let (text, point) = point_from_cursor("match(\n tab@\n)");
428+
let document = Document::new(text.as_str(), None);
429+
let document_context = DocumentContext::new(&document, point, None);
430+
let state = WorldState::default();
431+
let context = CompletionContext::new(&document_context, &state);
432+
let completions = completions_from_call(&context).unwrap().unwrap();
433+
434+
assert_eq!(completions.len(), 4);
435+
assert_eq!(completions.get(0).unwrap().label, "x = ");
436+
assert_eq!(completions.get(1).unwrap().label, "table = ");
437+
438+
// Partially typed second argument
439+
let (text, point) = point_from_cursor("match(\n 1,\n tab@\n)");
440+
let document = Document::new(text.as_str(), None);
441+
let document_context = DocumentContext::new(&document, point, None);
442+
let state = WorldState::default();
443+
let context = CompletionContext::new(&document_context, &state);
444+
let completions = completions_from_call(&context).unwrap().unwrap();
445+
446+
assert_eq!(completions.len(), 4);
447+
assert_eq!(completions.get(0).unwrap().label, "x = ");
448+
assert_eq!(completions.get(1).unwrap().label, "table = ");
449+
})
450+
}
451+
452+
#[test]
453+
fn test_completions_in_value_position() {
454+
r_task(|| {
455+
fn assert_no_call_completions(code_with_cursor: &str) {
456+
let (text, point) = point_from_cursor(code_with_cursor);
457+
let document = Document::new(text.as_str(), None);
458+
let document_context = DocumentContext::new(&document, point, None);
459+
let state = WorldState::default();
460+
let context = CompletionContext::new(&document_context, &state);
461+
let completions = completions_from_call(&context).unwrap();
462+
463+
// We shouldn't get completions in value position
464+
assert!(completions.is_none());
465+
}
466+
467+
// Single line, with space
468+
assert_no_call_completions("match(x = @)");
469+
470+
// Single line, no space
471+
assert_no_call_completions("match(x =@)");
472+
473+
// Multiline case, with space
474+
assert_no_call_completions("match(\n x = @\n)");
475+
476+
// Multiline case, no space
477+
assert_no_call_completions("match(\n x =@\n)");
478+
});
479+
}
416480
}

0 commit comments

Comments
 (0)