Skip to content

Commit b037c6d

Browse files
authored
Refactor completions (#754)
* Hello CompletionBuilder * Move completions_from_unique_sources() into the builder * Move completions_from_composite_sources() into the builder * Opening move re: using parameter_hints from the builder * Let's try an extension trait (namespace) * Use extension trait (search_path) * Use extension trait (workspace) * Move pipe root into the builder and make it lazy * Get pipe root from the builder (not passing as arg) * Opening move for a single interface for completion sources * Put unique and composite completion aggregation logic back where it started * Fully shift to CompletionSource approach for unique sources * Make CompletionSource trait object-safe by adding &self parameter Allows us to iterate in a clean way over unique sources * Adopt CompletionSource trait for composite sources Finally all completion sources are shifted over to a new, common pattern. Some return types were changed (to Option<Vec<CompletionItem>>) as a move towards consistency in completion source functions * Update copyright headers * More consistent with pattern used by other completion sources * Consolidate imports * Be more consistent and intentional around logging for unique sources * Borrow some ideas from unique for composite sources * More work on logging, mostly for composite sources Remove on-entry logging for completion sources, since the 2 main aggregators now log before trying each source Shift towards debug for logging from a completion source or related helpers * Make parameter hints lazy * Remove superficial differences in handling of parameter hints and pipe root * This hasn't been dead code for a while * Can have same visiblity as composite * Rescue existing comments * Can be private * Restore / add some hints * Use a more restricted visibility * Tweak comment * The Great Renaming * CompletionBuilder -> CompletionContext * context -> document_context (where necessary for disambiguation) * Get rid of the loops, but use common helpers for each "iteration" / source * Use `log::trace!`, not `log::debug!` * Don't import `anyhow::Result` * Go back to marshalling unique and composite completions via free functions * Rename to push_completions() * Encapsulate pipe root error handling in its accessor * Remove unnecessary re-export of find_pipe_root
1 parent d7026cd commit b037c6d

26 files changed

+600
-264
lines changed

crates/ark/src/lsp/completions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//
66
//
77

8+
mod completion_context;
89
mod completion_item;
910
mod parameter_hints;
1011
mod provide;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// completion_context.rs
3+
//
4+
// Copyright (C) 2025 Posit Software, PBC. All rights reserved.
5+
//
6+
//
7+
8+
use std::cell::OnceCell;
9+
10+
use crate::lsp::completions::parameter_hints::ParameterHints;
11+
use crate::lsp::completions::parameter_hints::{self};
12+
use crate::lsp::completions::sources::composite::pipe::find_pipe_root;
13+
use crate::lsp::completions::sources::composite::pipe::PipeRoot;
14+
use crate::lsp::document_context::DocumentContext;
15+
use crate::lsp::state::WorldState;
16+
17+
pub(crate) struct CompletionContext<'a> {
18+
pub(crate) document_context: &'a DocumentContext<'a>,
19+
pub(crate) state: &'a WorldState,
20+
parameter_hints_cell: OnceCell<ParameterHints>,
21+
pipe_root_cell: OnceCell<Option<PipeRoot>>,
22+
}
23+
24+
impl<'a> CompletionContext<'a> {
25+
pub fn new(document_context: &'a DocumentContext, state: &'a WorldState) -> Self {
26+
Self {
27+
document_context,
28+
state,
29+
parameter_hints_cell: OnceCell::new(),
30+
pipe_root_cell: OnceCell::new(),
31+
}
32+
}
33+
34+
pub fn parameter_hints(&self) -> &ParameterHints {
35+
self.parameter_hints_cell.get_or_init(|| {
36+
parameter_hints::parameter_hints(
37+
self.document_context.node,
38+
&self.document_context.document.contents,
39+
)
40+
})
41+
}
42+
43+
pub fn pipe_root(&self) -> Option<PipeRoot> {
44+
self.pipe_root_cell
45+
.get_or_init(|| match find_pipe_root(self.document_context) {
46+
Ok(root) => root,
47+
Err(e) => {
48+
log::error!("Error trying to find pipe root: {}", e);
49+
None
50+
},
51+
})
52+
.clone()
53+
}
54+
}

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

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use std::fs::DirEntry;
99

1010
use anyhow::bail;
11-
use anyhow::Result;
1211
use harp::r_symbol;
1312
use harp::utils::is_symbol_valid;
1413
use harp::utils::r_env_binding_is_active;
@@ -52,23 +51,23 @@ use crate::treesitter::NodeTypeExt;
5251
pub(super) fn completion_item(
5352
label: impl AsRef<str>,
5453
data: CompletionData,
55-
) -> Result<CompletionItem> {
54+
) -> anyhow::Result<CompletionItem> {
5655
Ok(CompletionItem {
5756
label: label.as_ref().to_string(),
5857
data: Some(serde_json::to_value(data)?),
5958
..Default::default()
6059
})
6160
}
6261

63-
pub(super) fn completion_item_from_file(entry: DirEntry) -> Result<CompletionItem> {
62+
pub(super) fn completion_item_from_file(entry: DirEntry) -> anyhow::Result<CompletionItem> {
6463
let name = entry.file_name().to_string_lossy().to_string();
6564
let mut item = completion_item(name, CompletionData::File { path: entry.path() })?;
6665

6766
item.kind = Some(CompletionItemKind::FILE);
6867
Ok(item)
6968
}
7069

71-
pub(super) fn completion_item_from_directory(entry: DirEntry) -> Result<CompletionItem> {
70+
pub(super) fn completion_item_from_directory(entry: DirEntry) -> anyhow::Result<CompletionItem> {
7271
let mut name = entry.file_name().to_string_lossy().to_string();
7372
name.push('/');
7473

@@ -84,7 +83,7 @@ pub(super) fn completion_item_from_directory(entry: DirEntry) -> Result<Completi
8483
Ok(item)
8584
}
8685

87-
pub(super) fn completion_item_from_direntry(entry: DirEntry) -> Result<CompletionItem> {
86+
pub(super) fn completion_item_from_direntry(entry: DirEntry) -> anyhow::Result<CompletionItem> {
8887
let is_dir = entry
8988
.file_type()
9089
.map(|value| value.is_dir())
@@ -99,7 +98,7 @@ pub(super) fn completion_item_from_direntry(entry: DirEntry) -> Result<Completio
9998
pub(super) fn completion_item_from_assignment(
10099
node: &Node,
101100
context: &DocumentContext,
102-
) -> Result<CompletionItem> {
101+
) -> anyhow::Result<CompletionItem> {
103102
let lhs = node.child_by_field_name("lhs").into_result()?;
104103
let rhs = node.child_by_field_name("rhs").into_result()?;
105104

@@ -143,7 +142,7 @@ pub(super) fn completion_item_from_assignment(
143142
pub(super) unsafe fn completion_item_from_package(
144143
package: &str,
145144
append_colons: bool,
146-
) -> Result<CompletionItem> {
145+
) -> anyhow::Result<CompletionItem> {
147146
let mut item = completion_item(package.to_string(), CompletionData::Package {
148147
name: package.to_string(),
149148
})?;
@@ -170,8 +169,8 @@ pub(super) unsafe fn completion_item_from_package(
170169
pub(super) fn completion_item_from_function(
171170
name: &str,
172171
package: Option<&str>,
173-
parameter_hints: ParameterHints,
174-
) -> Result<CompletionItem> {
172+
parameter_hints: &ParameterHints,
173+
) -> anyhow::Result<CompletionItem> {
175174
let label = format!("{}", name);
176175
let mut item = completion_item(label, CompletionData::Function {
177176
name: name.to_string(),
@@ -219,7 +218,7 @@ fn item_details(package: Option<&str>) -> CompletionItemLabelDetails {
219218
}
220219

221220
// TODO
222-
pub(super) unsafe fn completion_item_from_dataset(name: &str) -> Result<CompletionItem> {
221+
pub(super) unsafe fn completion_item_from_dataset(name: &str) -> anyhow::Result<CompletionItem> {
223222
let mut item = completion_item(name.to_string(), CompletionData::Unknown)?;
224223
item.kind = Some(CompletionItemKind::STRUCT);
225224
Ok(item)
@@ -229,7 +228,7 @@ pub(super) unsafe fn completion_item_from_data_variable(
229228
name: &str,
230229
owner: &str,
231230
enquote: bool,
232-
) -> Result<CompletionItem> {
231+
) -> anyhow::Result<CompletionItem> {
233232
let mut item = completion_item(name.to_string(), CompletionData::DataVariable {
234233
name: name.to_string(),
235234
owner: owner.to_string(),
@@ -253,8 +252,8 @@ pub(super) unsafe fn completion_item_from_object(
253252
envir: SEXP,
254253
package: Option<&str>,
255254
promise_strategy: PromiseStrategy,
256-
parameter_hints: ParameterHints,
257-
) -> Result<CompletionItem> {
255+
parameter_hints: &ParameterHints,
256+
) -> anyhow::Result<CompletionItem> {
258257
if r_typeof(object) == PROMSXP {
259258
return completion_item_from_promise(
260259
name,
@@ -303,8 +302,8 @@ pub(super) unsafe fn completion_item_from_promise(
303302
envir: SEXP,
304303
package: Option<&str>,
305304
promise_strategy: PromiseStrategy,
306-
parameter_hints: ParameterHints,
307-
) -> Result<CompletionItem> {
305+
parameter_hints: &ParameterHints,
306+
) -> anyhow::Result<CompletionItem> {
308307
if r_promise_is_forced(object) {
309308
// Promise has already been evaluated before.
310309
// Generate completion item from underlying value.
@@ -352,7 +351,7 @@ pub(super) unsafe fn completion_item_from_promise(
352351
Ok(item)
353352
}
354353

355-
pub(super) fn completion_item_from_active_binding(name: &str) -> Result<CompletionItem> {
354+
pub(super) fn completion_item_from_active_binding(name: &str) -> anyhow::Result<CompletionItem> {
356355
// We never want to force active bindings, so we return a fairly
357356
// generic completion item
358357
let mut item = completion_item(name, CompletionData::Object {
@@ -373,8 +372,8 @@ pub(super) unsafe fn completion_item_from_namespace(
373372
name: &str,
374373
namespace: SEXP,
375374
package: &str,
376-
parameter_hints: ParameterHints,
377-
) -> Result<CompletionItem> {
375+
parameter_hints: &ParameterHints,
376+
) -> anyhow::Result<CompletionItem> {
378377
// First, look in the namespace itself.
379378
if let Some(item) = completion_item_from_symbol(
380379
name,
@@ -410,7 +409,7 @@ pub(super) unsafe fn completion_item_from_lazydata(
410409
name: &str,
411410
env: SEXP,
412411
package: &str,
413-
) -> Result<CompletionItem> {
412+
) -> anyhow::Result<CompletionItem> {
414413
// Important to use `Simple` here, as lazydata bindings are calls to `lazyLoadDBfetch()`
415414
// but we don't want to force them during completion generation because they often take a
416415
// long time to load.
@@ -419,7 +418,8 @@ pub(super) unsafe fn completion_item_from_lazydata(
419418
// Lazydata objects are never functions, so this doesn't really matter
420419
let parameter_hints = ParameterHints::Enabled;
421420

422-
match completion_item_from_symbol(name, env, Some(package), promise_strategy, parameter_hints) {
421+
match completion_item_from_symbol(name, env, Some(package), promise_strategy, &parameter_hints)
422+
{
423423
Some(item) => item,
424424
None => {
425425
// Should be impossible, but we'll be extra safe
@@ -433,8 +433,8 @@ pub(super) unsafe fn completion_item_from_symbol(
433433
envir: SEXP,
434434
package: Option<&str>,
435435
promise_strategy: PromiseStrategy,
436-
parameter_hints: ParameterHints,
437-
) -> Option<Result<CompletionItem>> {
436+
parameter_hints: &ParameterHints,
437+
) -> Option<anyhow::Result<CompletionItem>> {
438438
let symbol = r_symbol!(name);
439439

440440
match r_env_binding_is_active(envir, symbol) {
@@ -475,7 +475,7 @@ pub(super) unsafe fn completion_item_from_symbol(
475475
pub(super) fn completion_item_from_scope_parameter(
476476
parameter: &str,
477477
_context: &DocumentContext,
478-
) -> Result<CompletionItem> {
478+
) -> anyhow::Result<CompletionItem> {
479479
let mut item = completion_item(parameter, CompletionData::ScopeParameter {
480480
name: parameter.to_string(),
481481
})?;
@@ -488,7 +488,7 @@ pub(super) fn completion_item_from_parameter(
488488
parameter: &str,
489489
callee: &str,
490490
context: &DocumentContext,
491-
) -> Result<CompletionItem> {
491+
) -> anyhow::Result<CompletionItem> {
492492
if parameter == "..." {
493493
return completion_item_from_dot_dot_dot(callee, context);
494494
}
@@ -522,7 +522,7 @@ pub(super) fn completion_item_from_parameter(
522522
fn completion_item_from_dot_dot_dot(
523523
callee: &str,
524524
context: &DocumentContext,
525-
) -> Result<CompletionItem> {
525+
) -> anyhow::Result<CompletionItem> {
526526
// Special behavior for `...` arguments, where we want to show them
527527
// in quick suggestions (to show help docs for them), but not actually
528528
// insert any text for them if the user selects them. Can't use an

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
//
2+
// parameter_hints.rs
3+
//
4+
// Copyright (C) 2025 Posit Software, PBC. All rights reserved.
5+
//
6+
//
7+
18
use ropey::Rope;
29
use tree_sitter::Node;
310

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
//
22
// provide.rs
33
//
4-
// Copyright (C) 2023 Posit Software, PBC. All rights reserved.
4+
// Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
55
//
66
//
77

8-
use anyhow::Result;
98
use tower_lsp::lsp_types::CompletionItem;
109

11-
use crate::lsp::completions::parameter_hints::parameter_hints;
12-
use crate::lsp::completions::sources::completions_from_composite_sources;
13-
use crate::lsp::completions::sources::completions_from_unique_sources;
10+
use crate::lsp::completions::completion_context::CompletionContext;
11+
use crate::lsp::completions::sources::composite;
12+
use crate::lsp::completions::sources::unique;
1413
use crate::lsp::document_context::DocumentContext;
1514
use crate::lsp::state::WorldState;
1615

1716
// Entry point for completions.
1817
// Must be within an `r_task()`.
1918
pub(crate) fn provide_completions(
20-
context: &DocumentContext,
19+
document_context: &DocumentContext,
2120
state: &WorldState,
22-
) -> Result<Vec<CompletionItem>> {
21+
) -> anyhow::Result<Vec<CompletionItem>> {
2322
log::info!("provide_completions()");
2423

25-
let parameter_hints = parameter_hints(context.node, &context.document.contents);
24+
let completion_context = CompletionContext::new(document_context, state);
2625

27-
if let Some(completions) = completions_from_unique_sources(context, parameter_hints)? {
26+
// Try unique sources first
27+
if let Some(completions) = unique::get_completions(&completion_context)? {
2828
return Ok(completions);
29-
};
29+
}
3030

3131
// At this point we aren't in a "unique" completion case, so just return a
32-
// set of reasonable completions based on loaded packages, the open
33-
// document, the current workspace, and any call related arguments
34-
completions_from_composite_sources(context, state, parameter_hints)
32+
// set of reasonable completions from composite sources
33+
Ok(composite::get_completions(&completion_context)?.unwrap_or_default())
3534
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77

88
use anyhow::bail;
9-
use anyhow::Result;
109
use stdext::*;
1110
use tower_lsp::lsp_types::CompletionItem;
1211
use tower_lsp::lsp_types::Documentation;
@@ -16,7 +15,7 @@ use tower_lsp::lsp_types::MarkupKind;
1615
use crate::lsp::completions::types::CompletionData;
1716
use crate::lsp::help::RHtmlHelp;
1817

19-
pub fn resolve_completion(item: &mut CompletionItem) -> Result<bool> {
18+
pub fn resolve_completion(item: &mut CompletionItem) -> anyhow::Result<bool> {
2019
let Some(data) = item.data.clone() else {
2120
bail!("Completion '{}' has no associated data", item.label);
2221
};
@@ -46,7 +45,10 @@ pub fn resolve_completion(item: &mut CompletionItem) -> Result<bool> {
4645
}
4746
}
4847

49-
fn resolve_package_completion_item(item: &mut CompletionItem, package: &str) -> Result<bool> {
48+
fn resolve_package_completion_item(
49+
item: &mut CompletionItem,
50+
package: &str,
51+
) -> anyhow::Result<bool> {
5052
let topic = join!(package, "-package");
5153
let help = unwrap!(RHtmlHelp::from_topic(topic.as_str(), Some(package))?, None => {
5254
return Ok(false);
@@ -68,7 +70,7 @@ fn resolve_function_completion_item(
6870
item: &mut CompletionItem,
6971
name: &str,
7072
package: Option<&str>,
71-
) -> Result<bool> {
73+
) -> anyhow::Result<bool> {
7274
let help = unwrap!(RHtmlHelp::from_function(name, package)?, None => {
7375
return Ok(false);
7476
});
@@ -90,7 +92,7 @@ fn resolve_parameter_completion_item(
9092
item: &mut CompletionItem,
9193
name: &str,
9294
function: &str,
93-
) -> Result<bool> {
95+
) -> anyhow::Result<bool> {
9496
// Get help for this function.
9597
let help = unwrap!(RHtmlHelp::from_function(function, None)?, None => {
9698
return Ok(false);

0 commit comments

Comments
 (0)