Skip to content

Commit

Permalink
Fix Sys.getenv() completions, support Sys.unsetenv() too (#530)
Browse files Browse the repository at this point in the history
* Strip out `= <default>` from argument text

* Test `Sys.getenv()`, `Sys.setenv()`, `options()`, and `getOption()`

* Support `Sys.unsetenv()`

* Add named argument tests

* Update crates/ark/src/lsp/completions/sources/unique/custom.rs

Co-authored-by: Lionel Henry <[email protected]>

* Use `parse_eval_base()`

* Be consistent with `parameter` naming within `custom.rs`

---------

Co-authored-by: Lionel Henry <[email protected]>
  • Loading branch information
DavisVaughan and lionel- authored Sep 23, 2024
1 parent 0223ef4 commit 753efaf
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 11 deletions.
248 changes: 237 additions & 11 deletions crates/ark/src/lsp/completions/sources/unique/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ pub fn completions_from_custom_source_impl(
//
// cf. https://github.com/posit-dev/positron/issues/3467
if index >= parameters.len() {
lsp::log_error!("Index {index} is out of bounds of the arguments of `{name}`");
lsp::log_error!("Index {index} is out of bounds of the parameters of `{name}`");
return Ok(None);
}
let parameter = parameters.get(index).into_result()?;

// Extract the argument text.
let argument = match parameter.label.clone() {
// Extract the parameter text.
let parameter = match parameter.label.clone() {
tower_lsp::lsp_types::ParameterLabel::LabelOffsets([start, end]) => {
let label = signature.label.as_str();
let substring = label.get((start as usize)..(end as usize));
Expand All @@ -106,7 +106,14 @@ pub fn completions_from_custom_source_impl(
tower_lsp::lsp_types::ParameterLabel::Simple(string) => string,
};

// Trim off the function arguments from the signature.
// Parameter text typically contains the parameter name and its default value if there is one.
// Extract out just the parameter name for matching purposes.
let parameter = match parameter.find("=") {
Some(loc) => &parameter[..loc].trim(),
None => parameter.as_str(),
};

// Trim off the function parameters from the signature.
if let Some(index) = name.find('(') {
name = name[0..index].to_string();
}
Expand Down Expand Up @@ -145,7 +152,7 @@ pub fn completions_from_custom_source_impl(
// Call our custom completion function.
let r_completions = RFunction::from(".ps.completions.getCustomCallCompletions")
.param("name", name)
.param("argument", argument)
.param("argument", parameter)
.param("position", position)
.call()?;

Expand Down Expand Up @@ -218,16 +225,17 @@ pub fn completions_from_custom_source_impl(
mod tests {
use tree_sitter::Point;

use crate::lsp::completions::sources::unique::custom::completions_from_custom_source_impl;
use crate::lsp::completions::sources::unique::custom::completions_from_custom_source;
use crate::lsp::document_context::DocumentContext;
use crate::lsp::documents::Document;
use crate::test::point_from_cursor;
use crate::test::r_test;

#[test]
fn test_completion_custom_library() {
r_test(|| {
let n_packages = {
let n = harp::parse_eval_global("length(base::.packages(TRUE))").unwrap();
let n = harp::parse_eval_base("length(base::.packages(TRUE))").unwrap();
let n = i32::try_from(n).unwrap();
usize::try_from(n).unwrap()
};
Expand All @@ -236,7 +244,7 @@ mod tests {
let document = Document::new("library()", None);
let context = DocumentContext::new(&document, point, None);

let n_compls = completions_from_custom_source_impl(&context)
let n_compls = completions_from_custom_source(&context)
.unwrap()
.unwrap()
.len();
Expand All @@ -248,11 +256,229 @@ mod tests {
let document = Document::new("library(uti)", None);
let context = DocumentContext::new(&document, point, None);

let compls = completions_from_custom_source_impl(&context)
.unwrap()
.unwrap();
let compls = completions_from_custom_source(&context).unwrap().unwrap();

assert!(compls.iter().any(|c| c.label == "utils"));
})
}

#[test]
fn test_completion_custom_sys_getenv() {
r_test(|| {
let name = "ARK_TEST_ENVVAR";

harp::parse_eval_base(format!("Sys.setenv({name} = '1')").as_str()).unwrap();

let assert_has_ark_test_envvar_completion = |text: &str, point: Point| {
let document = Document::new(text, None);
let context = DocumentContext::new(&document, point, None);

let completions = completions_from_custom_source(&context).unwrap().unwrap();
let completion = completions
.into_iter()
.find(|completion| completion.label == name);
assert!(completion.is_some());

// Insert text is quoted!
let completion = completion.unwrap();
assert_eq!(completion.insert_text.unwrap(), format!("\"{name}\""));
};

// Inside the parentheses
let (text, point) = point_from_cursor("Sys.getenv(@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Named argument
let (text, point) = point_from_cursor("Sys.getenv(x = @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Typed some and then requested completions
let (text, point) = point_from_cursor("Sys.getenv(ARK_@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// After a named argument
let (text, point) = point_from_cursor("Sys.getenv(unset = '1', @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Should not have it here
let (text, point) = point_from_cursor("Sys.getenv('foo', @)");
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_custom_source(&context).unwrap();
assert!(completions.is_none());

harp::parse_eval_base(format!("Sys.unsetenv('{name}')").as_str()).unwrap();
})
}

#[test]
fn test_completion_custom_sys_unsetenv() {
r_test(|| {
let name = "ARK_TEST_ENVVAR";

harp::parse_eval_base(format!("Sys.setenv({name} = '1')").as_str()).unwrap();

let assert_has_ark_test_envvar_completion = |text: &str, point: Point| {
let document = Document::new(text, None);
let context = DocumentContext::new(&document, point, None);

let completions = completions_from_custom_source(&context).unwrap().unwrap();
let completion = completions
.into_iter()
.find(|completion| completion.label == name);
assert!(completion.is_some());

// Insert text is quoted!
let completion = completion.unwrap();
assert_eq!(completion.insert_text.unwrap(), format!("\"{name}\""));
};

// Inside the parentheses
let (text, point) = point_from_cursor("Sys.unsetenv(@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Named argument
let (text, point) = point_from_cursor("Sys.unsetenv(x = @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Typed some and then requested completions
let (text, point) = point_from_cursor("Sys.unsetenv(ARK_@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// TODO: Technically `Sys.unsetenv()` takes a character vector, so we should probably provide
// completions for this too, but it probably isn't that common in practice
let (text, point) = point_from_cursor("Sys.unsetenv(c(@))");
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_custom_source(&context).unwrap();
assert!(completions.is_none());

harp::parse_eval_base(format!("Sys.unsetenv('{name}')").as_str()).unwrap();
})
}

#[test]
fn test_completion_custom_sys_setenv() {
r_test(|| {
let name = "ARK_TEST_ENVVAR";

harp::parse_eval_base(format!("Sys.setenv({name} = '1')").as_str()).unwrap();

let assert_has_ark_test_envvar_completion = |text: &str, point: Point| {
let document = Document::new(text, None);
let context = DocumentContext::new(&document, point, None);

let completions = completions_from_custom_source(&context).unwrap().unwrap();
let completion = completions
.into_iter()
.find(|completion| completion.label == name);
assert!(completion.is_some());

// Insert text is NOT quoted! And we get an ` = ` appended.
let completion = completion.unwrap();
assert_eq!(completion.insert_text.unwrap(), format!("{name} = "));
};

// Inside the parentheses
let (text, point) = point_from_cursor("Sys.setenv(@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Typed some and then requested completions
let (text, point) = point_from_cursor("Sys.setenv(ARK_@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Should have it here too, this takes `...`
let (text, point) = point_from_cursor("Sys.setenv(foo = 'bar', @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

harp::parse_eval_base(format!("Sys.unsetenv('{name}')").as_str()).unwrap();
})
}

#[test]
fn test_completion_custom_get_option() {
r_test(|| {
let name = "ARK_TEST_OPTION";

harp::parse_eval_base(format!("options({name} = '1')").as_str()).unwrap();

let assert_has_ark_test_envvar_completion = |text: &str, point: Point| {
let document = Document::new(text, None);
let context = DocumentContext::new(&document, point, None);

let completions = completions_from_custom_source(&context).unwrap().unwrap();
let completion = completions
.into_iter()
.find(|completion| completion.label == name);
assert!(completion.is_some());

// Insert text is quoted!
let completion = completion.unwrap();
assert_eq!(completion.insert_text.unwrap(), format!("\"{name}\""));
};

// Inside the parentheses
let (text, point) = point_from_cursor("getOption(@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Named argument
let (text, point) = point_from_cursor("getOption(x = @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Typed some and then requested completions
let (text, point) = point_from_cursor("getOption(ARK_@)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// After a named argument
let (text, point) = point_from_cursor("getOption(default = '1', @)");
assert_has_ark_test_envvar_completion(text.as_str(), point);

// Should not have it here
let (text, point) = point_from_cursor("getOption('foo', @)");
let document = Document::new(text.as_str(), None);
let context = DocumentContext::new(&document, point, None);
let completions = completions_from_custom_source(&context).unwrap();
assert!(completions.is_none());

harp::parse_eval_base(format!("options({name} = NULL)").as_str()).unwrap();
})
}

#[test]
fn test_completion_custom_options() {
r_test(|| {
let name = "ARK_TEST_OPTION";

harp::parse_eval_base(format!("options({name} = '1')").as_str()).unwrap();

let assert_has_ark_test_option_completion = |text: &str, point: Point| {
let document = Document::new(text, None);
let context = DocumentContext::new(&document, point, None);

let completions = completions_from_custom_source(&context).unwrap().unwrap();
let completion = completions
.into_iter()
.find(|completion| completion.label == name);
assert!(completion.is_some());

// Insert text is NOT quoted! And we get an ` = ` appended.
let completion = completion.unwrap();
assert_eq!(completion.insert_text.unwrap(), format!("{name} = "));
};

// Inside the parentheses
let (text, point) = point_from_cursor("options(@)");
assert_has_ark_test_option_completion(text.as_str(), point);

// Typed some and then requested completions
let (text, point) = point_from_cursor("options(ARK_@)");
assert_has_ark_test_option_completion(text.as_str(), point);

// Should have it here too, this takes `...`
let (text, point) = point_from_cursor("options(foo = 'bar', @)");
assert_has_ark_test_option_completion(text.as_str(), point);

harp::parse_eval_base(format!("options({name} = NULL)").as_str()).unwrap();
})
}
}
9 changes: 9 additions & 0 deletions crates/ark/src/modules/positron/completions.R
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ customCompletionHandlers <- new.env(parent = emptyenv())
)
})

.ps.completions.registerCustomCompletionHandler("base", "Sys.unsetenv", "x", function(position) {
.ps.completions.createCustomCompletions(
values = names(Sys.getenv()),
kind = "unknown",
enquote = TRUE,
append = ""
)
})

.ps.completions.registerCustomCompletionHandler("base", "Sys.setenv", "...", function(position) {

if (position != "name")
Expand Down

0 comments on commit 753efaf

Please sign in to comment.