use crate::autocomplete::jq_functions::JQ_FUNCTION_METADATA;
pub fn detect_function_at_cursor(query: &str, cursor_pos: usize) -> Option<&'static str> {
if query.is_empty() {
return None;
}
let chars: Vec<char> = query.chars().collect();
let len = chars.len();
if cursor_pos > len {
return None;
}
if let Some(func) = detect_function_at_word(&chars, cursor_pos) {
return Some(func);
}
find_enclosing_function(&chars, cursor_pos)
}
fn detect_function_at_word(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let (start, end) = find_word_boundaries(chars, cursor_pos);
if start == end {
return None;
}
let token: String = chars[start..end].iter().collect();
lookup_function(&token)
}
fn find_enclosing_function(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let mut depth: i32 = 0;
let scan_start = cursor_pos.min(chars.len());
for i in (0..scan_start).rev() {
match chars[i] {
')' => depth += 1,
'(' => {
depth -= 1;
if depth < 0 {
if let Some(func) = find_function_before_paren(chars, i) {
return Some(func);
}
depth = 0;
}
}
_ => {}
}
}
None
}
fn find_function_before_paren(chars: &[char], paren_pos: usize) -> Option<&'static str> {
if paren_pos == 0 {
return None;
}
let mut end = paren_pos;
while end > 0 && chars[end - 1].is_whitespace() {
end -= 1;
}
if end == 0 || !is_word_char(chars[end - 1]) {
return None;
}
let mut start = end - 1;
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1;
}
let token: String = chars[start..end].iter().collect();
lookup_function(&token)
}
fn find_word_boundaries(chars: &[char], cursor_pos: usize) -> (usize, usize) {
let len = chars.len();
if len == 0 {
return (0, 0);
}
let check_pos = if cursor_pos >= len {
if len > 0 {
len - 1
} else {
return (0, 0);
}
} else if !is_word_char(chars[cursor_pos]) {
if cursor_pos > 0 && is_word_char(chars[cursor_pos - 1]) {
cursor_pos - 1
} else {
return (0, 0);
}
} else {
cursor_pos
};
if !is_word_char(chars[check_pos]) {
return (0, 0);
}
let mut start = check_pos;
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1;
}
let mut end = check_pos + 1;
while end < len && is_word_char(chars[end]) {
end += 1;
}
(start, end)
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn lookup_function(token: &str) -> Option<&'static str> {
JQ_FUNCTION_METADATA
.iter()
.find(|f| f.name == token)
.map(|f| f.name)
}
pub fn detect_operator_at_cursor(query: &str, cursor_pos: usize) -> Option<&'static str> {
if query.is_empty() {
return None;
}
let chars: Vec<char> = query.chars().collect();
let len = chars.len();
if cursor_pos < len
&& let Some(op) = detect_operator_at_position(&chars, cursor_pos)
{
return Some(op);
}
if cursor_pos > 0
&& let Some(op) = detect_operator_at_position(&chars, cursor_pos - 1)
{
return Some(op);
}
None
}
fn detect_operator_at_position(chars: &[char], pos: usize) -> Option<&'static str> {
let len = chars.len();
if pos >= len {
return None;
}
let current = chars[pos];
if !matches!(current, '/' | '|' | '=' | '.') {
return None;
}
if let Some(op) = check_triple_slash_equals(chars, pos) {
return Some(op);
}
if let Some(op) = check_double_slash(chars, pos) {
return Some(op);
}
if let Some(op) = check_pipe_equals(chars, pos) {
return Some(op);
}
if let Some(op) = check_double_dot(chars, pos) {
return Some(op);
}
None
}
fn check_triple_slash_equals(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let len = chars.len();
let current = chars[cursor_pos];
match current {
'/' => {
if cursor_pos + 2 < len && chars[cursor_pos + 1] == '/' && chars[cursor_pos + 2] == '='
{
return Some("//=");
}
if cursor_pos > 0
&& cursor_pos + 1 < len
&& chars[cursor_pos - 1] == '/'
&& chars[cursor_pos + 1] == '='
{
return Some("//=");
}
}
'=' => {
if cursor_pos >= 2 && chars[cursor_pos - 1] == '/' && chars[cursor_pos - 2] == '/' {
return Some("//=");
}
}
_ => {}
}
None
}
fn check_double_slash(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let len = chars.len();
let current = chars[cursor_pos];
if current != '/' {
return None;
}
if cursor_pos + 1 < len && chars[cursor_pos + 1] == '/' {
if cursor_pos + 2 >= len || chars[cursor_pos + 2] != '=' {
return Some("//");
}
}
if cursor_pos > 0 && chars[cursor_pos - 1] == '/' {
if cursor_pos + 1 >= len || chars[cursor_pos + 1] != '=' {
return Some("//");
}
}
None
}
fn check_pipe_equals(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let len = chars.len();
let current = chars[cursor_pos];
match current {
'|' => {
if cursor_pos + 1 < len && chars[cursor_pos + 1] == '=' {
return Some("|=");
}
}
'=' => {
if cursor_pos > 0 && chars[cursor_pos - 1] == '|' {
return Some("|=");
}
}
_ => {}
}
None
}
fn check_double_dot(chars: &[char], cursor_pos: usize) -> Option<&'static str> {
let len = chars.len();
let current = chars[cursor_pos];
if current != '.' {
return None;
}
if cursor_pos + 1 < len && chars[cursor_pos + 1] == '.' {
if cursor_pos + 2 >= len || chars[cursor_pos + 2] != '.' {
if cursor_pos == 0 || chars[cursor_pos - 1] != '.' {
return Some("..");
}
}
}
if cursor_pos > 0 && chars[cursor_pos - 1] == '.' {
if cursor_pos + 1 >= len || chars[cursor_pos + 1] != '.' {
if cursor_pos < 2 || chars[cursor_pos - 2] != '.' {
return Some("..");
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn test_detect_function_cursor_on_function() {
assert_eq!(detect_function_at_cursor("select(.x)", 3), Some("select"));
assert_eq!(detect_function_at_cursor("select(.x)", 0), Some("select"));
assert_eq!(detect_function_at_cursor("select(.x)", 6), Some("select"));
}
#[test]
fn test_detect_function_multiple_functions_on_name() {
let query = "map(select(.x))";
assert_eq!(detect_function_at_cursor(query, 1), Some("map"));
assert_eq!(detect_function_at_cursor(query, 5), Some("select"));
}
#[test]
fn test_detect_function_with_pipe() {
let query = ".[] | sort | reverse";
assert_eq!(detect_function_at_cursor(query, 7), Some("sort"));
assert_eq!(detect_function_at_cursor(query, 15), Some("reverse"));
}
#[test]
fn test_detect_function_underscore_names() {
assert_eq!(detect_function_at_cursor("sort_by(.x)", 3), Some("sort_by"));
assert_eq!(
detect_function_at_cursor("to_entries", 5),
Some("to_entries")
);
}
#[test]
fn test_detect_enclosing_function_simple() {
assert_eq!(
detect_function_at_cursor("select(.field)", 8),
Some("select")
);
assert_eq!(
detect_function_at_cursor("select(.field)", 7),
Some("select")
);
}
#[test]
fn test_detect_enclosing_function_nested() {
let query = "select(.field | test(\"pattern\"))";
assert_eq!(detect_function_at_cursor(query, 25), Some("test"));
assert_eq!(detect_function_at_cursor(query, 10), Some("select"));
}
#[test]
fn test_detect_enclosing_function_deeply_nested() {
let query = "map(select(.x | test(\"a\") | contains(\"b\")))";
assert_eq!(detect_function_at_cursor(query, 38), Some("contains"));
assert_eq!(detect_function_at_cursor(query, 22), Some("test"));
assert_eq!(detect_function_at_cursor(query, 12), Some("select"));
assert_eq!(detect_function_at_cursor(query, 4), Some("select"));
}
#[test]
fn test_detect_enclosing_after_autocomplete() {
assert_eq!(detect_function_at_cursor("select(", 7), Some("select"));
assert_eq!(detect_function_at_cursor(".[] | map(", 10), Some("map"));
}
#[test]
fn test_detect_enclosing_with_content() {
assert_eq!(
detect_function_at_cursor("select(.name == \"foo\")", 15),
Some("select")
);
assert_eq!(
detect_function_at_cursor("map(.price * 2)", 10),
Some("map")
);
}
#[test]
fn test_detect_function_empty_query() {
assert_eq!(detect_function_at_cursor("", 0), None);
assert_eq!(detect_function_at_cursor("", 5), None);
}
#[test]
fn test_detect_function_cursor_outside_bounds() {
assert_eq!(detect_function_at_cursor("map", 100), None);
}
#[test]
fn test_detect_function_unknown_word() {
assert_eq!(detect_function_at_cursor("foo", 1), None);
assert_eq!(detect_function_at_cursor("foo(.x)", 5), None);
}
#[test]
fn test_detect_no_function_context() {
assert_eq!(detect_function_at_cursor(".field", 3), None);
assert_eq!(detect_function_at_cursor(".[0]", 2), None);
assert_eq!(detect_function_at_cursor(".a | .b", 5), None);
}
#[test]
fn test_cursor_at_word_end() {
assert_eq!(detect_function_at_cursor("map", 3), Some("map"));
assert_eq!(detect_function_at_cursor("select", 6), Some("select"));
}
#[test]
fn test_detect_operator_double_slash() {
assert_eq!(
detect_operator_at_cursor(".x // \"default\"", 3),
Some("//")
);
assert_eq!(
detect_operator_at_cursor(".x // \"default\"", 4),
Some("//")
);
}
#[test]
fn test_detect_operator_pipe_equals() {
assert_eq!(detect_operator_at_cursor(".x |= . + 1", 3), Some("|="));
assert_eq!(detect_operator_at_cursor(".x |= . + 1", 4), Some("|="));
}
#[test]
fn test_detect_operator_triple_slash_equals() {
assert_eq!(detect_operator_at_cursor(".x //= 0", 3), Some("//="));
assert_eq!(detect_operator_at_cursor(".x //= 0", 4), Some("//="));
assert_eq!(detect_operator_at_cursor(".x //= 0", 5), Some("//="));
}
#[test]
fn test_detect_operator_double_dot() {
assert_eq!(detect_operator_at_cursor(".. | numbers", 0), Some(".."));
assert_eq!(detect_operator_at_cursor(".. | numbers", 1), Some(".."));
}
#[test]
fn test_detect_operator_no_false_positive_single_slash() {
assert_eq!(detect_operator_at_cursor(".x / 2", 3), None);
}
#[test]
fn test_detect_operator_no_false_positive_single_pipe() {
assert_eq!(detect_operator_at_cursor(".x | .y", 3), None);
}
#[test]
fn test_detect_operator_no_false_positive_single_dot() {
assert_eq!(detect_operator_at_cursor(".field", 0), None);
assert_eq!(detect_operator_at_cursor(".x.y", 2), None);
}
#[test]
fn test_detect_operator_empty_query() {
assert_eq!(detect_operator_at_cursor("", 0), None);
}
#[test]
fn test_detect_operator_cursor_outside_bounds() {
assert_eq!(detect_operator_at_cursor("//", 100), None);
}
#[test]
fn test_detect_operator_at_query_boundaries() {
assert_eq!(detect_operator_at_cursor("// \"default\"", 0), Some("//"));
assert_eq!(detect_operator_at_cursor("// \"default\"", 1), Some("//"));
assert_eq!(detect_operator_at_cursor(".x //", 3), Some("//"));
assert_eq!(detect_operator_at_cursor(".x //", 4), Some("//"));
}
#[test]
fn test_detect_operator_triple_dot_not_detected() {
assert_eq!(detect_operator_at_cursor("...", 0), None);
assert_eq!(detect_operator_at_cursor("...", 1), None);
assert_eq!(detect_operator_at_cursor("...", 2), None);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_function_detection_on_name(
func_index in 0usize..JQ_FUNCTION_METADATA.len(),
prefix in "[.| ]{0,5}",
suffix in "[()| .]{0,10}",
cursor_offset in 0usize..20
) {
let func = &JQ_FUNCTION_METADATA[func_index];
let func_name = func.name;
let query = format!("{}{}{}", prefix, func_name, suffix);
let func_start = prefix.len();
let func_end = func_start + func_name.len();
if cursor_offset < func_name.len() {
let cursor_pos = func_start + cursor_offset;
let result = detect_function_at_cursor(&query, cursor_pos);
prop_assert_eq!(
result,
Some(func_name),
"Cursor at position {} in '{}' should detect function '{}'",
cursor_pos,
query,
func_name
);
}
let result_at_end = detect_function_at_cursor(&query, func_end);
prop_assert_eq!(
result_at_end,
Some(func_name),
"Cursor at end position {} in '{}' should detect function '{}'",
func_end,
query,
func_name
);
}
#[test]
fn prop_enclosing_function_detection(
func_index in 0usize..JQ_FUNCTION_METADATA.len(),
inner_content in "[.a-z0-9]{1,10}",
cursor_offset in 0usize..10
) {
let func = &JQ_FUNCTION_METADATA[func_index];
if !func.needs_parens {
return Ok(());
}
let func_name = func.name;
let query = format!("{}({})", func_name, inner_content);
let paren_pos = func_name.len();
let content_start = paren_pos + 1;
let content_end = content_start + inner_content.len();
let cursor_pos = content_start + (cursor_offset % inner_content.len().max(1));
if cursor_pos < content_end {
let result = detect_function_at_cursor(&query, cursor_pos);
prop_assert_eq!(
result,
Some(func_name),
"Cursor at position {} inside '{}' should detect enclosing function '{}'",
cursor_pos,
query,
func_name
);
}
}
#[test]
fn prop_nested_returns_innermost(
outer_index in 0usize..JQ_FUNCTION_METADATA.len(),
inner_index in 0usize..JQ_FUNCTION_METADATA.len()
) {
let outer = &JQ_FUNCTION_METADATA[outer_index];
let inner = &JQ_FUNCTION_METADATA[inner_index];
if !outer.needs_parens || !inner.needs_parens {
return Ok(());
}
let query = format!("{}({}(.x))", outer.name, inner.name);
let inner_content_pos = outer.name.len() + 1 + inner.name.len() + 1;
let result = detect_function_at_cursor(&query, inner_content_pos);
prop_assert_eq!(
result,
Some(inner.name),
"Cursor inside inner function in '{}' should detect '{}', not '{}'",
query,
inner.name,
outer.name
);
}
#[test]
fn prop_empty_query_returns_none(cursor_pos in 0usize..100) {
let result = detect_function_at_cursor("", cursor_pos);
prop_assert_eq!(result, None, "Empty query should always return None");
}
#[test]
fn prop_cursor_outside_bounds_returns_none(
query in "[a-z]{1,10}",
extra_offset in 1usize..100
) {
let len = query.chars().count();
let cursor_pos = len + extra_offset;
let result = detect_function_at_cursor(&query, cursor_pos);
prop_assert_eq!(
result,
None,
"Cursor at position {} (beyond query length {}) should return None",
cursor_pos,
len
);
}
#[test]
fn prop_operator_detection_correctness(
op_index in 0usize..4,
prefix in "[a-z ]{0,5}",
suffix in "[ a-z0-9\"]{0,10}",
cursor_offset in 0usize..3
) {
let operators = ["//", "|=", "//=", ".."];
let op = operators[op_index];
let query = format!("{}{}{}", prefix, op, suffix);
let op_start = prefix.len();
let op_len = op.len();
if op == ".." && suffix.starts_with('.') {
return Ok(());
}
if op == "//" && suffix.starts_with('=') {
return Ok(());
}
if cursor_offset < op_len {
let cursor_pos = op_start + cursor_offset;
let result = detect_operator_at_cursor(&query, cursor_pos);
prop_assert_eq!(
result,
Some(op),
"Cursor at position {} in '{}' should detect operator '{}'",
cursor_pos,
query,
op
);
}
}
#[test]
fn prop_no_false_positives_single_chars(
char_index in 0usize..3,
prefix in "[a-z0-9]{1,5}",
suffix in "[a-z0-9]{1,5}"
) {
let single_chars = ['/', '|', '.'];
let single_char = single_chars[char_index];
let query = format!("{}{}{}", prefix, single_char, suffix);
let char_pos = prefix.len();
let result = detect_operator_at_cursor(&query, char_pos);
prop_assert_eq!(
result,
None,
"Single '{}' at position {} in '{}' should NOT be detected as operator",
single_char,
char_pos,
query
);
}
#[test]
fn prop_multi_char_detection_order(
prefix in "[a-z ]{0,5}",
suffix in "[ a-z0-9]{0,5}",
cursor_offset in 0usize..3
) {
let query = format!("{}//={}", prefix, suffix);
let op_start = prefix.len();
let cursor_pos = op_start + cursor_offset;
let result = detect_operator_at_cursor(&query, cursor_pos);
prop_assert_eq!(
result,
Some("//="),
"Cursor at position {} in '{}' should detect '//=' not '//'",
cursor_pos,
query
);
}
}
}