use super::string_context::{complete_path_in_string, detect_string_context};
use crate::fuzzy::fuzzy_match;
use reedline::{Completer, Span, Suggestion};
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct CompletionCache {
token: String,
completions: Vec<String>,
timestamp: Instant,
}
#[derive(Debug, PartialEq)]
struct NamespaceToken {
package: String,
partial: String,
triple_colon: bool,
start_pos: usize,
}
fn parse_namespace_token(line: &str, cursor_pos: usize) -> Option<NamespaceToken> {
let before_cursor = &line[..cursor_pos.min(line.len())];
let partial: String = before_cursor
.chars()
.rev()
.take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
.collect::<String>()
.chars()
.rev()
.collect();
let before_partial = &before_cursor[..before_cursor.len() - partial.len()];
let (triple_colon, before_colons) = if let Some(rest) = before_partial.strip_suffix(":::") {
(true, rest)
} else if let Some(rest) = before_partial.strip_suffix("::") {
(false, rest)
} else {
return None;
};
let package: String = before_colons
.chars()
.rev()
.take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
.collect::<String>()
.chars()
.rev()
.collect();
if package.is_empty() {
return None;
}
if package.chars().next()?.is_ascii_digit() {
return None;
}
let colon_len = if triple_colon { 3 } else { 2 };
let start_pos = before_colons.len() - package.len();
if start_pos + package.len() + colon_len + partial.len() > cursor_pos {
return None;
}
Some(NamespaceToken {
package,
partial,
triple_colon,
start_pos,
})
}
struct NamespaceExportCache {
exports: Vec<String>,
timestamp: Instant,
}
struct NamespaceFuzzyCache {
input: String,
start_pos: usize,
suggestions: Vec<Suggestion>,
timestamp: Instant,
}
fn needs_backtick_quoting(name: &str) -> bool {
if name.is_empty() {
return false;
}
let mut chars = name.chars();
let first = chars.next().unwrap();
if !first.is_ascii_alphabetic() && first != '.' {
return true;
}
if first == '.'
&& let Some(second) = chars.next()
{
if second.is_ascii_digit() {
return true;
}
if !second.is_ascii_alphanumeric() && second != '.' && second != '_' {
return true;
}
}
for c in chars {
if !c.is_ascii_alphanumeric() && c != '.' && c != '_' {
return true;
}
}
false
}
fn has_special_suffix(s: &str) -> bool {
s.ends_with("::")
|| s.ends_with("<-")
|| s.ends_with("<<-")
|| s.ends_with("()")
|| s.ends_with('(')
|| s.ends_with('/')
|| s.ends_with(" = ")
|| s.ends_with("=")
}
#[derive(Debug, PartialEq)]
struct LibraryContext {
partial: String,
start_pos: usize,
}
fn detect_library_context(
line: &str,
cursor_pos: usize,
func_names: &[String],
) -> Option<LibraryContext> {
let pos = cursor_pos.min(line.len());
if !line.is_char_boundary(pos) {
return None;
}
let before_cursor = &line[..pos];
let mut paren_depth = 0;
let mut last_open_paren_pos = None;
for (i, c) in before_cursor.char_indices().rev() {
match c {
')' => paren_depth += 1,
'(' => {
if paren_depth == 0 {
last_open_paren_pos = Some(i);
break;
}
paren_depth -= 1;
}
_ => {}
}
}
let open_pos = last_open_paren_pos?;
let before_paren = before_cursor[..open_pos].trim_end();
let func_name = before_paren
.rsplit(|c: char| !c.is_alphanumeric() && c != '_' && c != '.' && c != ':')
.next()?;
if func_name.is_empty() {
return None;
}
let func_start = before_paren.len() - func_name.len();
if func_start > 0 {
let preceding_char = before_paren[..func_start].chars().next_back();
if matches!(preceding_char, Some('$' | '@')) {
return None;
}
}
if !func_names.iter().any(|f| f == func_name) {
return None;
}
let after_paren = &before_cursor[open_pos + 1..];
if after_paren.contains(',') {
return None;
}
let trimmed = after_paren.trim_start();
if trimmed.starts_with('"') || trimmed.starts_with('\'') {
return None;
}
let partial: String = trimmed
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '.' || *c == '_')
.collect();
let whitespace_len = after_paren.len() - trimmed.len();
let start_pos = open_pos + 1 + whitespace_len;
Some(LibraryContext { partial, start_pos })
}
pub struct RCompleter {
timeout_ms: u64,
debounce_ms: u64,
auto_paren_limit: usize,
cache: Option<CompletionCache>,
fuzzy_namespace: bool,
package_functions: Vec<String>,
namespace_cache: HashMap<String, NamespaceExportCache>,
namespace_fuzzy_cache: Option<NamespaceFuzzyCache>,
}
impl RCompleter {
pub fn new() -> Self {
RCompleter {
timeout_ms: 50,
debounce_ms: 100,
auto_paren_limit: 50,
cache: None,
fuzzy_namespace: false,
package_functions: vec!["library".to_string(), "require".to_string()],
namespace_cache: HashMap::new(),
namespace_fuzzy_cache: None,
}
}
pub fn with_settings_full(
timeout_ms: u64,
debounce_ms: u64,
auto_paren_limit: usize,
fuzzy_namespace: bool,
package_functions: Vec<String>,
) -> Self {
RCompleter {
timeout_ms,
debounce_ms,
auto_paren_limit,
cache: None,
fuzzy_namespace,
package_functions,
namespace_cache: HashMap::new(),
namespace_fuzzy_cache: None,
}
}
fn is_prefix_extension(&self, new_token: &str) -> bool {
if let Some(cache) = &self.cache {
let Some(extension) = new_token.strip_prefix(cache.token.as_str()) else {
return false;
};
!extension.is_empty()
&& extension
.chars()
.all(|c| c.is_alphanumeric() || c == '.' || c == '_')
} else {
false
}
}
fn should_use_cache(&self, token: &str) -> bool {
if let Some(cache) = &self.cache {
if cache.completions.is_empty() {
return false;
}
if token.is_empty() || cache.token.is_empty() {
return false;
}
if token.contains("::") || cache.token.contains("::") {
return false;
}
if cache.token == token {
cache.timestamp.elapsed() < Duration::from_millis(self.debounce_ms)
} else {
self.is_prefix_extension(token)
}
} else {
false
}
}
fn filter_cached(&self, token: &str) -> Vec<String> {
if let Some(cache) = &self.cache {
cache
.completions
.iter()
.filter(|c| c.starts_with(token))
.cloned()
.collect()
} else {
vec![]
}
}
fn invalidate_cache(&mut self) {
self.cache = None;
self.namespace_fuzzy_cache = None;
}
}
impl Default for RCompleter {
fn default() -> Self {
Self::new()
}
}
impl Completer for RCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
if let Some(ctx) = detect_string_context(line, pos) {
return complete_path_in_string(line, pos, &ctx);
}
if self.fuzzy_namespace
&& let Some(ns_token) = parse_namespace_token(line, pos)
{
let input = &line[ns_token.start_pos..pos];
if self.is_namespace_fuzzy_cache_hit(input, ns_token.start_pos) {
return self
.namespace_fuzzy_cache
.as_ref()
.unwrap()
.suggestions
.clone();
}
let suggestions = self.complete_namespace_fuzzy(&ns_token, pos);
self.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: input.to_string(),
start_pos: ns_token.start_pos,
suggestions: suggestions.clone(),
timestamp: Instant::now(),
});
return suggestions;
}
if self.fuzzy_namespace
&& let Some(lib_ctx) = detect_library_context(line, pos, &self.package_functions)
{
return self.complete_library_fuzzy(&lib_ctx, pos);
}
let token = arf_harp::completion::get_token(line, pos).unwrap_or_default();
let completions = if self.should_use_cache(&token) {
self.filter_cached(&token)
} else {
let fresh = match arf_harp::completion::get_completions(line, pos, self.timeout_ms) {
Ok(c) => c,
Err(_) => {
self.invalidate_cache();
return vec![];
}
};
if !fresh.is_empty() && !token.is_empty() {
self.cache = Some(CompletionCache {
token: token.clone(),
completions: fresh.clone(),
timestamp: Instant::now(),
});
}
fresh
};
if completions.is_empty() {
return vec![];
}
let filtered: Vec<String> = completions
.into_iter()
.filter(|c| c.starts_with(&token))
.collect();
if filtered.is_empty() {
return vec![];
}
let is_function = if self.auto_paren_limit > 0 {
self.check_function_types(&filtered)
} else {
vec![false; filtered.len()]
};
let match_len = token.len();
filtered
.into_iter()
.zip(is_function)
.filter(|(c, is_func)| {
c != &token || (*is_func && !has_special_suffix(c))
})
.map(|(c, is_func)| {
let start = pos - token.len();
let indices = if match_len > 0 {
Some((0..match_len).collect())
} else {
None
};
let (value, extra_info) = if is_func && !has_special_suffix(&c) {
(format!("{}()", c), Some("function".to_string()))
} else {
(c, None)
};
Suggestion {
value,
display_override: None,
description: extra_info,
extra: None,
span: Span { start, end: pos },
append_whitespace: false,
style: None,
match_indices: indices,
}
})
.collect()
}
}
impl RCompleter {
fn check_function_types(&self, completions: &[String]) -> Vec<bool> {
let check_count = completions.len().min(self.auto_paren_limit);
if check_count == 0 {
return vec![false; completions.len()];
}
let names_to_check: Vec<&str> = completions[..check_count]
.iter()
.map(|s| {
if has_special_suffix(s) {
""
} else {
s.as_str()
}
})
.collect();
let checked = arf_harp::completion::check_if_functions(&names_to_check).unwrap_or_default();
let mut result = checked;
result.resize(completions.len(), false);
result
}
const NAMESPACE_CACHE_DURATION: Duration = Duration::from_secs(300);
fn namespace_cache_key(pkg: &str, triple_colon: bool) -> String {
if triple_colon {
format!("{}:::", pkg)
} else {
format!("{}::", pkg)
}
}
fn is_namespace_fuzzy_cache_hit(&self, input: &str, start_pos: usize) -> bool {
if let Some(cache) = &self.namespace_fuzzy_cache {
cache.input == input
&& cache.start_pos == start_pos
&& cache.timestamp.elapsed() < Duration::from_millis(self.debounce_ms)
} else {
false
}
}
fn store_namespace_exports(&mut self, pkg: &str, triple_colon: bool, exports: Vec<String>) {
let cache_key = Self::namespace_cache_key(pkg, triple_colon);
if exports.is_empty() {
self.namespace_cache.remove(&cache_key);
return;
}
let ttl = Self::NAMESPACE_CACHE_DURATION;
self.namespace_cache
.retain(|_, v| v.timestamp.elapsed() < ttl);
self.namespace_cache.insert(
cache_key,
NamespaceExportCache {
exports,
timestamp: Instant::now(),
},
);
}
fn ensure_namespace_cached(&mut self, pkg: &str, triple_colon: bool) {
let cache_key = Self::namespace_cache_key(pkg, triple_colon);
if let Some(entry) = self.namespace_cache.get(&cache_key)
&& entry.timestamp.elapsed() < Self::NAMESPACE_CACHE_DURATION
{
return;
}
let exports =
arf_harp::completion::get_namespace_exports(pkg, triple_colon).unwrap_or_default();
self.store_namespace_exports(pkg, triple_colon, exports);
}
fn complete_namespace_fuzzy(
&mut self,
ns_token: &NamespaceToken,
pos: usize,
) -> Vec<Suggestion> {
self.ensure_namespace_cached(&ns_token.package, ns_token.triple_colon);
let cache_key = Self::namespace_cache_key(&ns_token.package, ns_token.triple_colon);
let exports = match self.namespace_cache.get(&cache_key) {
Some(entry) if !entry.exports.is_empty() => &entry.exports,
_ => return vec![],
};
let colons = if ns_token.triple_colon { ":::" } else { "::" };
let prefix = format!("{}{}", ns_token.package, colons);
let prefix_len = prefix.len();
let matched: Vec<(String, Option<Vec<usize>>, u32)> = if ns_token.partial.is_empty() {
let mut all: Vec<_> = exports.iter().map(|e| (e.clone(), None, 0u32)).collect();
all.sort_by(|a, b| a.0.cmp(&b.0));
all
} else {
let mut results: Vec<_> = exports
.iter()
.filter_map(|export| {
fuzzy_match(&ns_token.partial, export).map(|m| {
let indices: Vec<usize> =
m.indices.iter().map(|i| i + prefix_len).collect();
(export.clone(), Some(indices), m.score)
})
})
.collect();
results.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
results
};
if matched.is_empty() {
return vec![];
}
let matched: Vec<_> = matched
.into_iter()
.filter(|(export, _, _)| !export.contains('`'))
.collect();
if matched.is_empty() {
return vec![];
}
let qualified: Vec<(String, Option<Vec<usize>>)> = matched
.iter()
.map(|(export, indices, _)| {
if needs_backtick_quoting(export) {
let value = format!("{}`{}`", prefix, export);
let adjusted = indices
.as_ref()
.map(|idxs| idxs.iter().map(|&i| i + 1).collect());
(value, adjusted)
} else {
let value = format!("{}{}", prefix, export);
(value, indices.clone())
}
})
.collect();
let check_names: Vec<&str> = qualified
.iter()
.take(self.auto_paren_limit)
.map(|(name, _)| name.as_str())
.collect();
let is_function =
arf_harp::completion::check_if_functions(&check_names).unwrap_or_default();
let span = Span {
start: ns_token.start_pos,
end: pos,
};
qualified
.into_iter()
.zip(matched.iter())
.enumerate()
.map(|(i, ((base_value, indices), (export, _, _)))| {
let is_func = is_function.get(i).copied().unwrap_or(false);
let (value, extra_info) = if is_func && !has_special_suffix(export) {
(format!("{}()", base_value), Some("function".to_string()))
} else {
(base_value, None)
};
Suggestion {
value,
display_override: None,
description: extra_info,
extra: None,
span,
append_whitespace: false,
style: None,
match_indices: indices,
}
})
.collect()
}
fn complete_library_fuzzy(&self, lib_ctx: &LibraryContext, pos: usize) -> Vec<Suggestion> {
let packages = match arf_harp::completion::get_installed_packages() {
Ok(pkgs) => pkgs,
Err(_) => return vec![],
};
let span = Span {
start: lib_ctx.start_pos,
end: pos,
};
if lib_ctx.partial.is_empty() {
let mut sorted = packages;
sorted.sort();
return sorted
.into_iter()
.map(|pkg| Suggestion {
value: pkg,
display_override: None,
description: None,
extra: None,
span,
append_whitespace: false,
style: None,
match_indices: None,
})
.collect();
}
let mut results: Vec<_> = packages
.iter()
.filter_map(|pkg| {
fuzzy_match(&lib_ctx.partial, pkg).map(|m| (pkg.clone(), m.indices, m.score))
})
.collect();
results.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
results
.into_iter()
.map(|(pkg, indices, _)| Suggestion {
value: pkg,
display_override: None,
description: None,
extra: None,
span,
append_whitespace: false,
style: None,
match_indices: Some(indices),
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_namespace_token_basic() {
let result = parse_namespace_token("sf::geo", 7);
assert_eq!(
result,
Some(NamespaceToken {
package: "sf".to_string(),
partial: "geo".to_string(),
triple_colon: false,
start_pos: 0,
})
);
}
#[test]
fn test_parse_namespace_token_triple_colon() {
let result = parse_namespace_token("pkg:::func", 10);
assert_eq!(
result,
Some(NamespaceToken {
package: "pkg".to_string(),
partial: "func".to_string(),
triple_colon: true,
start_pos: 0,
})
);
}
#[test]
fn test_parse_namespace_token_empty_partial() {
let result = parse_namespace_token("stats::", 7);
assert_eq!(
result,
Some(NamespaceToken {
package: "stats".to_string(),
partial: "".to_string(),
triple_colon: false,
start_pos: 0,
})
);
}
#[test]
fn test_parse_namespace_token_in_expression() {
let result = parse_namespace_token("x <- dplyr::filt", 16);
assert_eq!(
result,
Some(NamespaceToken {
package: "dplyr".to_string(),
partial: "filt".to_string(),
triple_colon: false,
start_pos: 5,
})
);
}
#[test]
fn test_parse_namespace_token_no_match() {
assert_eq!(parse_namespace_token("hello", 5), None);
assert_eq!(parse_namespace_token("::", 2), None);
assert_eq!(parse_namespace_token("", 0), None);
}
#[test]
fn test_parse_namespace_token_dotted_package() {
let result = parse_namespace_token("data.table::set", 15);
assert_eq!(
result,
Some(NamespaceToken {
package: "data.table".to_string(),
partial: "set".to_string(),
triple_colon: false,
start_pos: 0,
})
);
}
#[test]
fn test_needs_backtick_quoting() {
assert!(!needs_backtick_quoting("filter"));
assert!(!needs_backtick_quoting("st_geometry"));
assert!(!needs_backtick_quoting("data.frame"));
assert!(!needs_backtick_quoting(".internal"));
assert!(!needs_backtick_quoting("my_func"));
assert!(needs_backtick_quoting("%>%"));
assert!(needs_backtick_quoting("%in%"));
assert!(needs_backtick_quoting("+.gg"));
assert!(needs_backtick_quoting("[.data.frame"));
assert!(needs_backtick_quoting("_private"));
assert!(needs_backtick_quoting(".2bad"));
assert!(!needs_backtick_quoting("."));
assert!(!needs_backtick_quoting(".."));
assert!(needs_backtick_quoting("données"));
assert!(needs_backtick_quoting("日本語"));
assert!(needs_backtick_quoting("café"));
assert!(needs_backtick_quoting("a`b"));
assert!(needs_backtick_quoting("`"));
assert!(!needs_backtick_quoting(""));
}
#[test]
fn test_namespace_cache_key_format() {
assert_eq!(RCompleter::namespace_cache_key("dplyr", false), "dplyr::");
assert_eq!(RCompleter::namespace_cache_key("dplyr", true), "dplyr:::");
}
#[test]
fn test_store_namespace_exports_caches_non_empty() {
let mut completer = RCompleter::new();
let exports = vec!["filter".to_string(), "mutate".to_string()];
completer.store_namespace_exports("dplyr", false, exports.clone());
let cached = completer.namespace_cache.get("dplyr::").unwrap();
assert_eq!(cached.exports, exports);
}
#[test]
fn test_store_namespace_exports_skips_empty() {
let mut completer = RCompleter::new();
completer.store_namespace_exports("nonexistent", false, vec![]);
assert!(!completer.namespace_cache.contains_key("nonexistent::"));
}
#[test]
fn test_store_namespace_exports_removes_stale_on_empty() {
let mut completer = RCompleter::new();
completer.store_namespace_exports("pkg", false, vec!["func".to_string()]);
assert!(completer.namespace_cache.contains_key("pkg::"));
completer.store_namespace_exports("pkg", false, vec![]);
assert!(!completer.namespace_cache.contains_key("pkg::"));
}
#[test]
fn test_store_namespace_exports_evicts_expired() {
let mut completer = RCompleter::new();
let expired_duration = RCompleter::NAMESPACE_CACHE_DURATION + Duration::from_secs(1);
let Some(expired_timestamp) = Instant::now().checked_sub(expired_duration) else {
return;
};
completer.namespace_cache.insert(
"old_pkg::".to_string(),
NamespaceExportCache {
exports: vec!["old_func".to_string()],
timestamp: expired_timestamp,
},
);
completer.store_namespace_exports("new_pkg", false, vec!["new_func".to_string()]);
assert!(!completer.namespace_cache.contains_key("old_pkg::"));
assert!(completer.namespace_cache.contains_key("new_pkg::"));
}
#[test]
fn test_store_namespace_exports_keeps_fresh_entries() {
let mut completer = RCompleter::new();
completer.store_namespace_exports("pkg_a", false, vec!["func_a".to_string()]);
completer.store_namespace_exports("pkg_b", false, vec!["func_b".to_string()]);
assert!(completer.namespace_cache.contains_key("pkg_a::"));
assert!(completer.namespace_cache.contains_key("pkg_b::"));
}
#[test]
fn test_separate_cache_for_double_and_triple_colon() {
let mut completer = RCompleter::new();
completer.store_namespace_exports("pkg", false, vec!["exported".to_string()]);
completer.store_namespace_exports(
"pkg",
true,
vec!["exported".to_string(), "internal".to_string()],
);
let double = completer.namespace_cache.get("pkg::").unwrap();
assert_eq!(double.exports, vec!["exported"]);
let triple = completer.namespace_cache.get("pkg:::").unwrap();
assert_eq!(triple.exports, vec!["exported", "internal"]);
}
#[test]
fn test_invalidate_cache_preserves_namespace_cache() {
let mut completer = RCompleter::new();
completer.store_namespace_exports("dplyr", false, vec!["filter".to_string()]);
completer.invalidate_cache();
assert!(completer.namespace_cache.contains_key("dplyr::"));
}
#[test]
fn test_invalidate_cache_clears_fuzzy_namespace_cache() {
let mut completer = RCompleter::new();
completer.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: "dplyr::filt".to_string(),
start_pos: 0,
suggestions: vec![],
timestamp: Instant::now(),
});
completer.invalidate_cache();
assert!(completer.namespace_fuzzy_cache.is_none());
}
#[test]
fn test_fuzzy_cache_hit_same_input_and_position() {
let mut completer = RCompleter::new();
completer.debounce_ms = 5000;
completer.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: "dplyr::filt".to_string(),
start_pos: 0,
suggestions: vec![],
timestamp: Instant::now(),
});
assert!(completer.is_namespace_fuzzy_cache_hit("dplyr::filt", 0));
}
#[test]
fn test_fuzzy_cache_miss_different_start_pos() {
let mut completer = RCompleter::new();
completer.debounce_ms = 5000;
completer.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: "dplyr::filt".to_string(),
start_pos: 0,
suggestions: vec![],
timestamp: Instant::now(),
});
assert!(!completer.is_namespace_fuzzy_cache_hit("dplyr::filt", 5));
}
#[test]
fn test_fuzzy_cache_miss_different_input() {
let mut completer = RCompleter::new();
completer.debounce_ms = 5000;
completer.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: "dplyr::filt".to_string(),
start_pos: 0,
suggestions: vec![],
timestamp: Instant::now(),
});
assert!(!completer.is_namespace_fuzzy_cache_hit("dplyr::filte", 0));
}
#[test]
fn test_fuzzy_cache_miss_when_empty() {
let completer = RCompleter::new();
assert!(!completer.is_namespace_fuzzy_cache_hit("dplyr::filt", 0));
}
#[test]
fn test_fuzzy_cache_miss_when_expired() {
let mut completer = RCompleter::new();
completer.debounce_ms = 0;
completer.namespace_fuzzy_cache = Some(NamespaceFuzzyCache {
input: "dplyr::filt".to_string(),
start_pos: 0,
suggestions: vec![],
timestamp: Instant::now(),
});
assert!(!completer.is_namespace_fuzzy_cache_hit("dplyr::filt", 0));
}
fn lib_funcs() -> Vec<String> {
vec!["library".to_string(), "require".to_string()]
}
#[test]
fn test_detect_library_context_library() {
let result = detect_library_context("library(dpl", 11, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "dpl".to_string(),
start_pos: 8,
})
);
}
#[test]
fn test_detect_library_context_require() {
let result = detect_library_context("require(gg", 10, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "gg".to_string(),
start_pos: 8,
})
);
}
#[test]
fn test_detect_library_context_comma_skipped() {
let result = detect_library_context("library(dplyr, ", 15, &lib_funcs());
assert_eq!(result, None);
}
#[test]
fn test_detect_library_context_quoted_skipped() {
let result = detect_library_context("library(\"dpl", 12, &lib_funcs());
assert_eq!(result, None);
}
#[test]
fn test_detect_library_context_box_use() {
let funcs = vec!["box::use".to_string()];
let result = detect_library_context("box::use(dpl", 12, &funcs);
assert_eq!(
result,
Some(LibraryContext {
partial: "dpl".to_string(),
start_pos: 9,
})
);
}
#[test]
fn test_detect_library_context_wrong_function() {
let result = detect_library_context("foo(bar", 7, &lib_funcs());
assert_eq!(result, None);
}
#[test]
fn test_detect_library_context_with_spaces() {
let result = detect_library_context(" library( dpl", 14, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "dpl".to_string(),
start_pos: 11,
})
);
}
#[test]
fn test_detect_library_context_empty_partial() {
let result = detect_library_context("x <- library(", 13, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "".to_string(),
start_pos: 13,
})
);
}
#[test]
fn test_detect_library_context_nested_parens() {
let result = detect_library_context("print(library(dpl", 17, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "dpl".to_string(),
start_pos: 14,
})
);
}
#[test]
fn test_detect_library_context_single_quote_skipped() {
let result = detect_library_context("library('dpl", 12, &lib_funcs());
assert_eq!(result, None);
}
#[test]
fn test_detect_library_context_stray_colons_no_match() {
assert_eq!(
detect_library_context("x:library(dpl", 13, &lib_funcs()),
None
);
assert_eq!(
detect_library_context(":::library(dpl", 14, &lib_funcs()),
None
);
}
#[test]
fn test_detect_library_context_non_ascii() {
let result = detect_library_context("library(données", 16, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "données".to_string(),
start_pos: 8,
})
);
}
#[test]
fn test_detect_library_context_utf8_boundary_safety() {
let line = "library(données";
let e_pos = line.find('é').unwrap();
let mid_byte = e_pos + 1; assert!(!line.is_char_boundary(mid_byte));
assert_eq!(detect_library_context(line, mid_byte, &lib_funcs()), None);
}
#[test]
fn test_detect_library_context_member_access_skipped() {
assert_eq!(
detect_library_context("obj$library(dpl", 15, &lib_funcs()),
None
);
assert_eq!(
detect_library_context("env@require(gg", 14, &lib_funcs()),
None
);
}
#[test]
fn test_detect_library_context_namespace_in_arg() {
let result = detect_library_context("library(pkg::something", 22, &lib_funcs());
assert_eq!(
result,
Some(LibraryContext {
partial: "pkg".to_string(),
start_pos: 8,
})
);
}
fn completer_with_cache(token: &str, completions: Vec<String>) -> RCompleter {
let mut c = RCompleter::new();
c.cache = Some(CompletionCache {
token: token.to_string(),
completions,
timestamp: Instant::now(),
});
c
}
#[test]
fn test_is_prefix_extension_identifier_only() {
let c = completer_with_cache("pr", vec!["print".to_string()]);
assert!(c.is_prefix_extension("pri"));
}
#[test]
fn test_is_prefix_extension_dot_underscore() {
let c = completer_with_cache("my_", vec!["my_func".to_string()]);
assert!(c.is_prefix_extension("my_f"));
let c = completer_with_cache("st.", vec!["st.geo".to_string()]);
assert!(c.is_prefix_extension("st.g"));
}
#[test]
fn test_is_prefix_extension_dollar_sign_not_extension() {
let c = completer_with_cache("l$", vec!["l$a".to_string(), "l$b".to_string()]);
assert!(!c.is_prefix_extension("l$a$"));
}
#[test]
fn test_is_prefix_extension_at_sign_not_extension() {
let c = completer_with_cache("obj@", vec!["obj@slot".to_string()]);
assert!(!c.is_prefix_extension("obj@slot$"));
}
#[test]
fn test_is_prefix_extension_same_token_not_extension() {
let c = completer_with_cache("pri", vec!["print".to_string()]);
assert!(!c.is_prefix_extension("pri"));
}
#[test]
fn test_is_prefix_extension_bracket_not_extension() {
let c = completer_with_cache("l", vec!["list".to_string()]);
assert!(!c.is_prefix_extension("l["));
}
#[test]
fn test_is_prefix_extension_colon_not_extension() {
let c = completer_with_cache("pkg:", vec!["pkg::foo".to_string()]);
assert!(!c.is_prefix_extension("pkg::foo"));
}
#[test]
fn test_is_prefix_extension_no_cache() {
let c = RCompleter::new();
assert!(!c.is_prefix_extension("pri"));
}
}