use alloc::collections::BTreeMap;
use super::types::{tok, CompletionContext, TokenCursor};
use crate::dsl::tokenizer::{Token, TokenKind};
pub(super) fn analyze_last_tokens(
tokens: &[Token],
cursor: &TokenCursor,
effect_fns: &BTreeMap<&str, &str>,
) -> CompletionContext {
let cursor_token_idx = cursor.token_index().unwrap_or(tokens.len());
match &tokens[..cursor_token_idx] {
[.., tok!(Identifier => obj), tok!(Dot)] => {
CompletionContext::DotAccess { receiver_type: obj.to_string() }
},
[.., tok!(Identifier => obj), tok!(Dot), tok!(Identifier)] => {
CompletionContext::DotAccess { receiver_type: obj.to_string() }
},
[.., tok!(RightParen), tok!(Dot)] => {
let closing_paren_idx = cursor_token_idx - 2;
let paren_idx = find_matching_opening_paren(tokens, closing_paren_idx).unwrap_or(0);
CompletionContext::DotAccess {
receiver_type: infer_return_type(tokens, paren_idx, effect_fns),
}
},
[.., tok!(RightParen), tok!(Dot), tok!(Identifier)] => {
let closing_paren_idx = cursor_token_idx - 3;
let paren_idx = find_matching_opening_paren(tokens, closing_paren_idx).unwrap_or(0);
CompletionContext::DotAccess {
receiver_type: infer_return_type(tokens, paren_idx, effect_fns),
}
},
[.., tok!(Identifier => ns), tok!(DoubleColon)] => {
CompletionContext::DoubleColon { namespace: ns.to_string() }
},
[.., tok!(Identifier => ns), tok!(DoubleColon), tok!(Identifier)] => {
CompletionContext::DoubleColon { namespace: ns.to_string() }
},
[.., tok!(Identifier => ns), tok!(DoubleColon), tok!(Identifier => fn_name), tok!(LeftParen)] => {
CompletionContext::FnCall {
fn_name: fn_name.to_string(),
namespace: Some(ns.to_string()),
arg_index: 0,
}
},
[.., tok!(Identifier => fn_name), tok!(LeftParen)] => CompletionContext::FnCall {
fn_name: fn_name.to_string(),
namespace: None,
arg_index: 0,
},
_tokens_slice => {
let mut paren_idx: Option<usize> = None;
let mut depth = 0;
for (idx, token) in tokens[..cursor_token_idx]
.iter()
.enumerate()
.rev()
{
match token.kind {
TokenKind::RightParen => depth += 1,
TokenKind::LeftParen => {
if depth == 0 {
paren_idx = Some(idx);
break;
}
depth -= 1;
},
_ => {},
}
}
if let Some(paren_idx) = paren_idx {
let mut comma_count = 0;
let mut depth = 1;
for token in &tokens[paren_idx + 1..cursor_token_idx] {
match token.kind {
TokenKind::LeftParen | TokenKind::LeftBracket => depth += 1,
TokenKind::RightParen | TokenKind::RightBracket => depth -= 1,
TokenKind::Comma if depth == 1 => comma_count += 1,
_ => {},
}
}
if paren_idx > 0 {
if let Some(tok!(Identifier => fn_name)) = tokens.get(paren_idx - 1) {
let namespace = if paren_idx >= 3 {
match (&tokens[paren_idx - 3], &tokens[paren_idx - 2]) {
(tok!(Identifier => ns), tok!(DoubleColon)) => Some(ns.to_string()),
_ => None,
}
} else {
None
};
return CompletionContext::FnCall {
fn_name: fn_name.to_string(),
namespace,
arg_index: comma_count,
};
}
}
}
if let Some(brace_idx) = tokens[..cursor_token_idx]
.iter()
.rposition(|t| t.kind == TokenKind::LeftBrace)
{
let mut depth = 1;
for token in &tokens[brace_idx + 1..cursor_token_idx] {
match token.kind {
TokenKind::LeftBrace => depth += 1,
TokenKind::RightBrace => {
depth -= 1;
if depth == 0 {
break;
}
},
_ => {},
}
}
if depth > 0 {
if brace_idx > 0 {
if let Some(tok!(Identifier => struct_name)) = tokens.get(brace_idx - 1) {
let filled_fields = tokens[brace_idx..cursor_token_idx]
.windows(2)
.filter_map(|w| match w {
[tok!(Identifier => field), tok!(Colon)] => {
Some(field.to_string())
},
_ => None,
})
.collect();
return CompletionContext::StructInit {
struct_name: struct_name.to_string(),
filled_fields,
};
}
}
}
}
CompletionContext::TopLevel
},
}
}
fn find_matching_opening_paren(tokens: &[Token], closing_paren_idx: usize) -> Option<usize> {
let mut depth = 1;
for (idx, token) in tokens[..closing_paren_idx]
.iter()
.enumerate()
.rev()
{
match token.kind {
TokenKind::RightParen => depth += 1,
TokenKind::LeftParen => {
depth -= 1;
if depth == 0 {
return Some(idx);
}
},
_ => {},
}
}
None
}
fn infer_return_type(
tokens: &[Token],
paren_idx: usize,
effect_fns: &BTreeMap<&str, &str>,
) -> String {
if paren_idx >= 3 {
if let [tok!(Identifier => namespace), tok!(DoubleColon), tok!(Identifier)] =
&tokens[paren_idx - 3..paren_idx]
{
return match *namespace {
"fx" => "Effect",
other => other, }
.to_string();
}
}
if paren_idx >= 2 {
if let [tok!(Dot), tok!(Identifier)] = &tokens[paren_idx - 2..paren_idx] {
if paren_idx >= 3 {
if let tok!(RightParen) = tokens[paren_idx - 3] {
if let Some(matching_paren) = find_matching_opening_paren(tokens, paren_idx - 3)
{
return infer_return_type(tokens, matching_paren, effect_fns);
}
}
}
}
}
if paren_idx >= 1 {
if let tok!(Identifier => fn_name) = &tokens[paren_idx - 1] {
return effect_fns
.get(fn_name)
.copied()
.unwrap_or("<Unknown>")
.to_string();
}
}
"<Unknown>".to_string()
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
use crate::dsl::tokenizer::{sanitize_tokens, tokenize};
fn assert_context_eq(input: &str, expected: &CompletionContext) {
fn analyze(input: &str) -> CompletionContext {
let tokens = tokenize(input).map(sanitize_tokens).unwrap();
let cursor = TokenCursor::from_tokens(&tokens, input.len() as _);
let effect_fns: BTreeMap<&str, &str> = BTreeMap::from([
("fade_from_fg", "Effect"),
("fade_to", "Effect"),
("dissolve", "Effect"),
("sweep_in", "Effect"),
("sweep_out", "Effect"),
("slide_in", "Effect"),
("slide_out", "Effect"),
("coalesce", "Effect"),
("paint", "Effect"),
("saturate", "Effect"),
("lighten", "Effect"),
("darken", "Effect"),
("hsl_shift", "Effect"),
])
.into_iter()
.collect();
analyze_last_tokens(&tokens, &cursor, &effect_fns)
}
let cursor_index = input
.char_indices()
.position(|(_, c)| c == '^')
.unwrap_or(input.len());
let ctx = analyze(&input[..cursor_index]);
assert_eq!(&ctx, expected, "For input: {input}");
}
#[test]
fn test_top_level_context() {
assert_context_eq("", &CompletionContext::TopLevel);
assert_context_eq("fx", &CompletionContext::TopLevel);
assert_context_eq(
"let c = Color::from_u32(0x1d2021);",
&CompletionContext::TopLevel,
);
}
#[test]
fn test_dot_access() {
assert_context_eq("a.", &CompletionContext::DotAccess {
receiver_type: "a".to_string(),
});
assert_context_eq("effect.", &CompletionContext::DotAccess {
receiver_type: "effect".to_string(),
});
assert_context_eq("a.clon", &CompletionContext::DotAccess {
receiver_type: "a".to_string(),
});
assert_context_eq("effect.with_cell", &CompletionContext::DotAccess {
receiver_type: "effect".to_string(),
});
assert_context_eq(
"Color::from_u32(0x1d2021).",
&CompletionContext::DotAccess { receiver_type: "Color".to_string() },
);
assert_context_eq(
indoc! {"
fx::fade_from(bg, bg, 1000). ^ // chevron is cursor pos
.with_color_space(ColorSpace::Rgb)
"},
&CompletionContext::DotAccess { receiver_type: "Effect".to_string() },
);
}
#[test]
fn test_double_colon() {
assert_context_eq("fx::", &CompletionContext::DoubleColon {
namespace: "fx".to_string(),
});
assert_context_eq("Color::", &CompletionContext::DoubleColon {
namespace: "Color".to_string(),
});
assert_context_eq(
indoc! {"
fx::^
fx::fade_from(Black, Black, 1000)
"},
&CompletionContext::DoubleColon { namespace: "fx".to_string() },
);
}
#[test]
fn test_function_call_no_args() {
assert_context_eq("fade_to(", &CompletionContext::FnCall {
fn_name: "fade_to".to_string(),
namespace: None,
arg_index: 0,
});
}
#[test]
fn test_function_call_with_args() {
assert_context_eq("fade_to(Color::Red,", &CompletionContext::FnCall {
fn_name: "fade_to".to_string(),
namespace: None,
arg_index: 1,
});
assert_context_eq("dissolve(500, CircOut,", &CompletionContext::FnCall {
fn_name: "dissolve".to_string(),
namespace: None,
arg_index: 2,
});
}
#[test]
fn test_struct_init() {
assert_context_eq("Rect {", &CompletionContext::StructInit {
struct_name: "Rect".to_string(),
filled_fields: vec![],
});
assert_context_eq("Rect { x: 0,", &CompletionContext::StructInit {
struct_name: "Rect".to_string(),
filled_fields: vec!["x".to_string()],
});
assert_context_eq("Rect { x: 0, y: 5,", &CompletionContext::StructInit {
struct_name: "Rect".to_string(),
filled_fields: vec!["x".to_string(), "y".to_string()],
});
assert_context_eq("Yolo { foo: 0, ba", &CompletionContext::StructInit {
struct_name: "Yolo".to_string(),
filled_fields: vec!["foo".to_string()],
});
}
#[test]
fn test_nested_function_calls() {
assert_context_eq("outer(inner(", &CompletionContext::FnCall {
fn_name: "inner".to_string(),
namespace: None,
arg_index: 0,
});
}
#[test]
fn test_method_chain() {
assert_context_eq("fx::dissolve(500).with_", &CompletionContext::DotAccess {
receiver_type: "Effect".to_string(),
});
assert_context_eq(
"Color::from_u32(0xff0000).",
&CompletionContext::DotAccess { receiver_type: "Color".to_string() },
);
assert_context_eq("Layout::horizontal([]).", &CompletionContext::DotAccess {
receiver_type: "Layout".to_string(),
});
assert_context_eq("Style::new().", &CompletionContext::DotAccess {
receiver_type: "Style".to_string(),
});
assert_context_eq("some_function().", &CompletionContext::DotAccess {
receiver_type: "<Unknown>".to_string(),
});
let src = indoc! {"
fx::fade_from_fg(Black, 1000)
.with_filter(CellFilter::All)
.with_pattern(DissolvePattern::default())
.with_color_space(ColorSpace::Rgb)
.re
"};
assert_context_eq(src, &CompletionContext::DotAccess {
receiver_type: "Effect".to_string(),
});
let src = indoc! {"
fade_from_fg(Black, 1000)
.with_filter(CellFilter::All)
.with_pattern(DissolvePattern::default())
.with_color_space(ColorSpace::Rgb)
.re
"};
assert_context_eq(src, &CompletionContext::DotAccess {
receiver_type: "Effect".to_string(),
});
}
#[test]
fn test_qualified_function_call() {
assert_context_eq("Color::from_u32(", &CompletionContext::FnCall {
fn_name: "from_u32".to_string(),
namespace: Some("Color".to_string()),
arg_index: 0,
});
}
#[test]
fn test_nested_function_calls_infer_type() {
assert_context_eq(
"fx::sequence(&[fx::dissolve(500)]).",
&CompletionContext::DotAccess { receiver_type: "Effect".to_string() },
);
assert_context_eq(
"fx::parallel(&[fx::sequence(&[fx::dissolve(500)])]).",
&CompletionContext::DotAccess { receiver_type: "Effect".to_string() },
);
}
#[test]
fn test_nested_effects_inside_slice() {
assert_context_eq("fx::sequence(&[", &CompletionContext::FnCall {
fn_name: "sequence".to_string(),
namespace: Some("fx".to_string()),
arg_index: 0,
});
assert_context_eq(
"fx::sequence(&[fx::dissolve(500), ",
&CompletionContext::FnCall {
fn_name: "sequence".to_string(),
namespace: Some("fx".to_string()),
arg_index: 0,
},
);
assert_context_eq(
"fx::sequence(&[fx::dissolve(500), fx::",
&CompletionContext::DoubleColon { namespace: "fx".to_string() },
);
assert_context_eq(
"fx::sequence(&[fx::dissolve(500), fx::consume_tick()]).",
&CompletionContext::DotAccess { receiver_type: "Effect".to_string() },
);
}
#[test]
fn test_extract_partial_token() {
let tokens = tokenize("Motion::Left").unwrap();
let tokens = sanitize_tokens(tokens);
let cursor_pos = tokens.last().unwrap().span.1;
let cursor = TokenCursor::from_tokens(&tokens, cursor_pos);
let partial = cursor.extract_partial_token(&tokens);
assert_eq!(partial, "Left");
let cursor_pos = tokens.last().unwrap().span.0 + 2;
let cursor = TokenCursor::from_tokens(&tokens, cursor_pos);
let partial = cursor.extract_partial_token(&tokens);
assert_eq!(partial, "Le");
}
}