Skip to content

Commit 09f4b1f

Browse files
authored
Migrate hover, signature help, and completions to ArkFile (#1264)
Branched from #1263 Progress towards #1212 The handlers migrated use a slightly different approach because they use `r_task()` heavily and the Oak DB shouldn't be touched from an `r_task()` because of the risk of cancellation panics, which would be UB over the R stack. We could catch panics in `r_task()` and unwind for cancellation but I didn't want to think through the various implications. - `DocumentContext` drops `Document` and stores the pieces it needs from the `ArkDb` and LSP state (`tree`, `contents`, `line_index`, `encoding`). This way the DB is not sent over an `r_task()`. - Add LSP <> TS position/range converters as methods on `DocumentContext` (parallel to the ones on `ArkFile`, also replacements for the `Document` methods). - Migrate hover, signature help, and completion to the refactored `DocumentContext`.
2 parents 362ca82 + 112e465 commit 09f4b1f

24 files changed

Lines changed: 350 additions & 293 deletions

crates/ark/src/lsp/ark_file.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,15 @@ impl ArkFile {
9090
db: &dyn ArkDb,
9191
point: tree_sitter::Point,
9292
) -> anyhow::Result<lsp_types::Position> {
93-
let line_col = biome_line_index::LineCol {
94-
line: point.row as u32,
95-
col: point.column as u32,
96-
};
97-
to_proto::position_from_line_col(line_col, self.line_index(db), self.encoding)
93+
lsp_position_from_tree_sitter_point(point, self.line_index(db), self.encoding)
9894
}
9995

10096
pub(crate) fn lsp_range_from_tree_sitter_range(
10197
&self,
10298
db: &dyn ArkDb,
10399
range: tree_sitter::Range,
104100
) -> anyhow::Result<lsp_types::Range> {
105-
let start = self.lsp_position_from_tree_sitter_point(db, range.start_point)?;
106-
let end = self.lsp_position_from_tree_sitter_point(db, range.end_point)?;
107-
Ok(lsp_types::Range::new(start, end))
101+
lsp_range_from_tree_sitter_range(range, self.line_index(db), self.encoding)
108102
}
109103

110104
pub(crate) fn tree_sitter_range_from_lsp_range(
@@ -126,6 +120,31 @@ impl ArkFile {
126120
}
127121
}
128122

123+
/// Free functions over `LineIndex` + `PositionEncoding`, so anything holding
124+
/// those two (an `ArkFile` plus its `db`, or a `DocumentContext`) can convert
125+
/// without each carrying its own copy of the logic.
126+
pub(crate) fn lsp_position_from_tree_sitter_point(
127+
point: tree_sitter::Point,
128+
line_index: &biome_line_index::LineIndex,
129+
encoding: PositionEncoding,
130+
) -> anyhow::Result<lsp_types::Position> {
131+
let line_col = biome_line_index::LineCol {
132+
line: point.row as u32,
133+
col: point.column as u32,
134+
};
135+
to_proto::position_from_line_col(line_col, line_index, encoding)
136+
}
137+
138+
pub(crate) fn lsp_range_from_tree_sitter_range(
139+
range: tree_sitter::Range,
140+
line_index: &biome_line_index::LineIndex,
141+
encoding: PositionEncoding,
142+
) -> anyhow::Result<lsp_types::Range> {
143+
let start = lsp_position_from_tree_sitter_point(range.start_point, line_index, encoding)?;
144+
let end = lsp_position_from_tree_sitter_point(range.end_point, line_index, encoding)?;
145+
Ok(lsp_types::Range::new(start, end))
146+
}
147+
129148
#[cfg(test)]
130149
pub(crate) fn test_ark_file(code: &str) -> (oak_db::OakDatabase, ArkFile) {
131150
use aether_path::FilePath;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use tower_lsp::lsp_types::Range;
3636
use tower_lsp::lsp_types::TextEdit;
3737
use tree_sitter::Node;
3838

39+
use crate::lsp::ark_file::lsp_position_from_tree_sitter_point;
3940
use crate::lsp::completions::function_context::ArgumentsStatus;
4041
use crate::lsp::completions::function_context::FunctionContext;
4142
use crate::lsp::completions::function_context::FunctionRefUsage;
@@ -100,7 +101,7 @@ pub(super) fn completion_item_from_assignment(
100101
let lhs = node.child_by_field_name("lhs").into_result()?;
101102
let rhs = node.child_by_field_name("rhs").into_result()?;
102103

103-
let label = lhs.node_as_str(&context.document.contents)?.to_string();
104+
let label = lhs.node_as_str(context.contents)?.to_string();
104105

105106
// TODO: Resolve functions that exist in-document here.
106107
let mut item = completion_item(label.clone(), CompletionData::ScopeVariable {
@@ -123,7 +124,7 @@ pub(super) fn completion_item_from_assignment(
123124
// benefit from the logic in completion_item_from_function() :(
124125
if rhs.node_type() == NodeType::FunctionDefinition {
125126
if let Some(parameters) = rhs.child_by_field_name("parameters") {
126-
let parameters = parameters.node_as_str(&context.document.contents)?;
127+
let parameters = parameters.node_as_str(context.contents)?;
127128
item.detail = Some(join!(label, parameters));
128129
}
129130

@@ -644,9 +645,8 @@ fn completion_item_from_dot_dot_dot(
644645

645646
item.kind = Some(CompletionItemKind::FIELD);
646647

647-
let position = context
648-
.document
649-
.lsp_position_from_tree_sitter_point(context.point)?;
648+
let position =
649+
lsp_position_from_tree_sitter_point(context.point, context.line_index, context.encoding)?;
650650

651651
let range = Range {
652652
start: position,

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

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use tower_lsp::lsp_types;
1010
use tower_lsp::lsp_types::Range;
1111
use tree_sitter::Node;
1212

13+
use crate::lsp::ark_file::lsp_position_from_tree_sitter_point;
14+
use crate::lsp::ark_file::lsp_range_from_tree_sitter_range;
1315
use crate::lsp::document_context::DocumentContext;
1416
use crate::lsp::traits::node::NodeExt;
1517
use crate::treesitter::node_find_parent_call;
@@ -61,9 +63,11 @@ impl FunctionContext {
6163
// We shouldn't ever attempt to instantiate a FunctionContext or
6264
// function-flavored CompletionItem in this degenerate case, but we
6365
// return a dummy FunctionContext just to be safe.
64-
let node_end = document_context
65-
.document
66-
.lsp_position_from_tree_sitter_point(completion_node.range().end_point)?;
66+
let node_end = lsp_position_from_tree_sitter_point(
67+
completion_node.range().end_point,
68+
document_context.line_index,
69+
document_context.encoding,
70+
)?;
6771

6872
return Ok(Self {
6973
name: String::new(),
@@ -74,10 +78,7 @@ impl FunctionContext {
7478
});
7579
};
7680

77-
let usage = determine_function_usage(
78-
&effective_function_node,
79-
&document_context.document.contents,
80-
);
81+
let usage = determine_function_usage(&effective_function_node, document_context.contents);
8182

8283
let function_name_node = if effective_function_node.is_namespace_operator() {
8384
// Note: this could be 'None', in the case of, e.g., `dplyr::@`
@@ -93,7 +94,7 @@ impl FunctionContext {
9394

9495
let name = match function_name_node {
9596
Some(node) => node
96-
.node_to_string(&document_context.document.contents)
97+
.node_to_string(document_context.contents)
9798
.unwrap_or_default(),
9899
None => String::new(),
99100
};
@@ -111,16 +112,18 @@ impl FunctionContext {
111112
Ok(Self {
112113
name,
113114
range: match function_name_node {
114-
Some(node) => document_context
115-
.document
116-
.lsp_range_from_tree_sitter_range(node.range())?,
115+
Some(node) => lsp_range_from_tree_sitter_range(
116+
node.range(),
117+
document_context.line_index,
118+
document_context.encoding,
119+
)?,
117120
None => {
118121
// Create a zero-width range at the end of the effective_function_node
119-
let node_end = document_context
120-
.document
121-
.lsp_position_from_tree_sitter_point(
122-
effective_function_node.range().end_point,
123-
)?;
122+
let node_end = lsp_position_from_tree_sitter_point(
123+
effective_function_node.range().end_point,
124+
document_context.line_index,
125+
document_context.encoding,
126+
)?;
124127
lsp_types::Range::new(node_end, node_end)
125128
},
126129
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(crate) fn provide_completions(
2525
"provide_completions() - Completion node text: '{node_text}', Node type: '{node_type:?}'",
2626
node_text = document_context
2727
.node
28-
.node_as_str(&document_context.document.contents)
28+
.node_as_str(document_context.contents)
2929
.unwrap_or_default(),
3030
node_type = document_context.node.node_type()
3131
);

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,7 @@ mod tests {
220220
use crate::lsp::completions::completion_context::CompletionContext;
221221
use crate::lsp::completions::sources::composite::get_completions;
222222
use crate::lsp::completions::sources::composite::is_identifier_like;
223-
use crate::lsp::document::Document;
224-
use crate::lsp::document_context::DocumentContext;
223+
use crate::lsp::document_context::TestDocument;
225224
use crate::lsp::state::WorldState;
226225
use crate::r_task;
227226
use crate::treesitter::NodeType;
@@ -235,8 +234,8 @@ mod tests {
235234
// identifiers that we provide completions for
236235
for keyword in ["if", "for", "while"] {
237236
let (text, point) = point_from_cursor(&format!("{keyword}@"));
238-
let document = Document::new(text.as_str(), None);
239-
let context = DocumentContext::new(&document, point, None);
237+
let doc = TestDocument::new(&text);
238+
let context = doc.context(point);
240239

241240
assert!(is_identifier_like(context.node));
242241
assert_eq!(
@@ -251,8 +250,8 @@ mod tests {
251250
fn test_get_completions_on_empty_document() {
252251
r_task(|| {
253252
let (text, point) = point_from_cursor("@");
254-
let document = Document::new(text.as_str(), None);
255-
let document_context = DocumentContext::new(&document, point, None);
253+
let doc = TestDocument::new(&text);
254+
let document_context = doc.context(point);
256255
let state = WorldState::default();
257256
let context = CompletionContext::new(&document_context, &state);
258257

@@ -269,8 +268,8 @@ mod tests {
269268
r_task(|| {
270269
let code = "x <- 1:3\n@\nrnorm(3)";
271270
let (text, point) = point_from_cursor(code);
272-
let document = Document::new(text.as_str(), None);
273-
let document_context = DocumentContext::new(&document, point, None);
271+
let doc = TestDocument::new(&text);
272+
let document_context = doc.context(point);
274273
let state = WorldState::default();
275274
let context = CompletionContext::new(&document_context, &state);
276275

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

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ fn completions_from_call(
7676
return Ok(None);
7777
};
7878

79-
let callee = callee.node_as_str(&document_context.document.contents)?;
79+
let callee = callee.node_as_str(document_context.contents)?;
8080

8181
// - Prefer `root` as the first argument if it exists
8282
// - Then fall back to looking it up, if possible
@@ -122,7 +122,7 @@ fn get_first_argument(context: &DocumentContext, node: &Node) -> anyhow::Result<
122122
return Ok(None);
123123
};
124124

125-
let text = value.node_as_str(&context.document.contents)?;
125+
let text = value.node_as_str(context.contents)?;
126126

127127
let options = RParseEvalOptions {
128128
forbid_function_calls: true,
@@ -280,8 +280,7 @@ mod tests {
280280
use crate::fixtures::point_from_cursor;
281281
use crate::lsp::completions::completion_context::CompletionContext;
282282
use crate::lsp::completions::sources::composite::call::completions_from_call;
283-
use crate::lsp::document::Document;
284-
use crate::lsp::document_context::DocumentContext;
283+
use crate::lsp::document_context::TestDocument;
285284
use crate::lsp::state::WorldState;
286285
use crate::r_task;
287286

@@ -290,8 +289,8 @@ mod tests {
290289
r_task(|| {
291290
// Right after `tab`
292291
let (text, point) = point_from_cursor("match(tab@)");
293-
let document = Document::new(text.as_str(), None);
294-
let document_context = DocumentContext::new(&document, point, None);
292+
let doc = TestDocument::new(&text);
293+
let document_context = doc.context(point);
295294
let state = WorldState::default();
296295
let context = CompletionContext::new(&document_context, &state);
297296
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -303,8 +302,8 @@ mod tests {
303302

304303
// Right after `tab`
305304
let (text, point) = point_from_cursor("match(1, tab@)");
306-
let document = Document::new(text.as_str(), None);
307-
let document_context = DocumentContext::new(&document, point, None);
305+
let doc = TestDocument::new(&text);
306+
let document_context = doc.context(point);
308307
let state = WorldState::default();
309308
let context = CompletionContext::new(&document_context, &state);
310309
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -323,8 +322,8 @@ mod tests {
323322
r_task(|| {
324323
// Place cursor between `()`
325324
let (text, point) = point_from_cursor("not_a_known_function(@)");
326-
let document = Document::new(text.as_str(), None);
327-
let document_context = DocumentContext::new(&document, point, None);
325+
let doc = TestDocument::new(&text);
326+
let document_context = doc.context(point);
328327
let state = WorldState::default();
329328
let context = CompletionContext::new(&document_context, &state);
330329
let completions = completions_from_call(&context).unwrap();
@@ -343,8 +342,8 @@ mod tests {
343342

344343
// Place cursor between `()`
345344
let (text, point) = point_from_cursor("my_fun(@)");
346-
let document = Document::new(text.as_str(), None);
347-
let document_context = DocumentContext::new(&document, point, None);
345+
let doc = TestDocument::new(&text);
346+
let document_context = doc.context(point);
348347
let state = WorldState::default();
349348
let context = CompletionContext::new(&document_context, &state);
350349
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -360,17 +359,17 @@ mod tests {
360359

361360
// Place just before the `()`
362361
let (text, point) = point_from_cursor("my_fun@()");
363-
let document = Document::new(text.as_str(), None);
364-
let document_context = DocumentContext::new(&document, point, None);
362+
let doc = TestDocument::new(&text);
363+
let document_context = doc.context(point);
365364
let state = WorldState::default();
366365
let context = CompletionContext::new(&document_context, &state);
367366
let completions = completions_from_call(&context).unwrap();
368367
assert!(completions.is_none());
369368

370369
// Place just after the `()`
371370
let (text, point) = point_from_cursor("my_fun()@");
372-
let document = Document::new(text.as_str(), None);
373-
let document_context = DocumentContext::new(&document, point, None);
371+
let doc = TestDocument::new(&text);
372+
let document_context = doc.context(point);
374373
let state = WorldState::default();
375374
let context = CompletionContext::new(&document_context, &state);
376375
let completions = completions_from_call(&context).unwrap();
@@ -392,8 +391,8 @@ mod tests {
392391

393392
// Place cursor between `()`
394393
let (text, point) = point_from_cursor("my_fun(@)");
395-
let document = Document::new(text.as_str(), None);
396-
let document_context = DocumentContext::new(&document, point, None);
394+
let doc = TestDocument::new(&text);
395+
let document_context = doc.context(point);
397396
let state = WorldState::default();
398397
let context = CompletionContext::new(&document_context, &state);
399398
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -409,8 +408,8 @@ mod tests {
409408
r_task(|| {
410409
// No arguments typed yet
411410
let (text, point) = point_from_cursor("match(\n @\n)");
412-
let document = Document::new(text.as_str(), None);
413-
let document_context = DocumentContext::new(&document, point, None);
411+
let doc = TestDocument::new(&text);
412+
let document_context = doc.context(point);
414413
let state = WorldState::default();
415414
let context = CompletionContext::new(&document_context, &state);
416415
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -421,8 +420,8 @@ mod tests {
421420

422421
// Partially typed argument
423422
let (text, point) = point_from_cursor("match(\n tab@\n)");
424-
let document = Document::new(text.as_str(), None);
425-
let document_context = DocumentContext::new(&document, point, None);
423+
let doc = TestDocument::new(&text);
424+
let document_context = doc.context(point);
426425
let state = WorldState::default();
427426
let context = CompletionContext::new(&document_context, &state);
428427
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -433,8 +432,8 @@ mod tests {
433432

434433
// Partially typed second argument
435434
let (text, point) = point_from_cursor("match(\n 1,\n tab@\n)");
436-
let document = Document::new(text.as_str(), None);
437-
let document_context = DocumentContext::new(&document, point, None);
435+
let doc = TestDocument::new(&text);
436+
let document_context = doc.context(point);
438437
let state = WorldState::default();
439438
let context = CompletionContext::new(&document_context, &state);
440439
let completions = completions_from_call(&context).unwrap().unwrap();
@@ -450,8 +449,8 @@ mod tests {
450449
r_task(|| {
451450
fn assert_no_call_completions(code_with_cursor: &str) {
452451
let (text, point) = point_from_cursor(code_with_cursor);
453-
let document = Document::new(text.as_str(), None);
454-
let document_context = DocumentContext::new(&document, point, None);
452+
let doc = TestDocument::new(&text);
453+
let document_context = doc.context(point);
455454
let state = WorldState::default();
456455
let context = CompletionContext::new(&document_context, &state);
457456
let completions = completions_from_call(&context).unwrap();

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ fn completions_from_document_function_arguments(
168168
continue;
169169
}
170170

171-
let parameter = node.node_as_str(&context.document.contents)?.to_string();
171+
let parameter = node.node_as_str(context.contents)?.to_string();
172172
match completion_item_from_scope_parameter(parameter.as_str(), context) {
173173
Ok(item) => completions.push(item),
174174
Err(err) => log::error!("{err:?}"),
@@ -185,7 +185,7 @@ fn call_uses_nse(node: &Node, context: &DocumentContext) -> bool {
185185
lhs.is_identifier_or_string().into_result()?;
186186

187187
let value = lhs
188-
.node_as_str(&context.document.contents)?
188+
.node_as_str(context.contents)?
189189
.to_string();
190190
matches!(value.as_str(), "expression" | "local" | "quote" | "enquote" | "substitute" | "with" | "within").into_result()?;
191191

0 commit comments

Comments
 (0)