use vize_croquis::macros::BUILTIN_MACROS;
pub fn is_macro_call_line(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.starts_with("import") {
return false;
}
for macro_name in BUILTIN_MACROS {
if let Some(pos) = line.find(macro_name) {
let after = &line[pos + macro_name.len()..];
let after_trimmed = after.trim_start();
let is_call = after_trimmed.starts_with('(') || after_trimmed.starts_with('<');
if !is_call {
continue;
}
let before = &line[..pos];
let single_quotes = count_unescaped_quotes(before, '\'');
let double_quotes = count_unescaped_quotes(before, '"');
let backticks = count_unescaped_quotes(before, '`');
if single_quotes % 2 == 1 || double_quotes % 2 == 1 || backticks % 2 == 1 {
continue;
}
return true;
}
}
false
}
fn count_unescaped_quotes(s: &str, quote_char: char) -> usize {
let mut count = 0;
let mut escaped = false;
for c in s.chars() {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == quote_char {
count += 1;
}
}
count
}
pub fn is_paren_macro_start(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.starts_with("import") {
return false;
}
for macro_name in BUILTIN_MACROS {
if let Some(pos) = line.find(macro_name) {
let after = &line[pos + macro_name.len()..];
let after_trimmed = after.trim_start();
let is_call = after_trimmed.starts_with('(') || after_trimmed.starts_with('<');
if !is_call {
continue;
}
let before = &line[..pos];
let single_quotes = count_unescaped_quotes(before, '\'');
let double_quotes = count_unescaped_quotes(before, '"');
let backticks = count_unescaped_quotes(before, '`');
if single_quotes % 2 == 1 || double_quotes % 2 == 1 || backticks % 2 == 1 {
continue;
}
if line.contains('(') {
let open_count = line.matches('(').count();
let close_count = line.matches(')').count();
if open_count > close_count {
return true;
}
}
}
}
false
}
pub fn is_multiline_macro_start(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.starts_with("import") {
return false;
}
for macro_name in BUILTIN_MACROS {
if let Some(pos) = line.find(macro_name) {
let after = &line[pos + macro_name.len()..];
let after_trimmed = after.trim_start();
let is_call = after_trimmed.starts_with('(') || after_trimmed.starts_with('<');
if !is_call {
continue;
}
let before = &line[..pos];
let single_quotes = count_unescaped_quotes(before, '\'');
let double_quotes = count_unescaped_quotes(before, '"');
let backticks = count_unescaped_quotes(before, '`');
if single_quotes % 2 == 1 || double_quotes % 2 == 1 || backticks % 2 == 1 {
continue;
}
if line.contains('<') {
let open_count = line.matches('<').count();
let close_count = line.matches('>').count();
if open_count > close_count {
return true;
}
let trimmed_no_semi = trimmed.trim_end_matches(';').trim_end();
if open_count == close_count
&& !trimmed_no_semi.ends_with("()")
&& !trimmed_no_semi.ends_with(')')
{
return true;
}
}
}
}
false
}
pub fn is_props_destructure_line(line: &str) -> bool {
let trimmed = line.trim();
(trimmed.starts_with("const {") || trimmed.starts_with("let {") || trimmed.starts_with("var {"))
&& (trimmed.contains("defineProps") || trimmed.contains("withDefaults"))
}
#[cfg(test)]
mod tests {
use super::{is_macro_call_line, is_multiline_macro_start, is_paren_macro_start};
#[test]
fn test_multiline_macro_start_complete_with_semicolon() {
assert!(
!is_multiline_macro_start(
"const layer = defineModel<ImageEffectorLayer>('layer', { required: true });"
),
"complete defineModel<Type>(args); should not be multi-line"
);
assert!(
!is_multiline_macro_start(
"const model = defineModel<string | number>({ required: true });"
),
"complete defineModel<union>(opts); should not be multi-line"
);
assert!(
!is_multiline_macro_start(
"const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });"
),
"complete defineModel<indexed type>(args); should not be multi-line"
);
}
#[test]
fn test_multiline_macro_start_genuinely_multiline() {
assert!(
is_multiline_macro_start("defineEmits<{"),
"unbalanced angle bracket should be multi-line"
);
assert!(
is_multiline_macro_start("const emit = defineEmits<{"),
"unbalanced with const should be multi-line"
);
}
#[test]
fn test_multiline_macro_start_complete_with_empty_parens() {
assert!(!is_multiline_macro_start(
"defineEmits<{ (e: 'click'): void }>()"
));
assert!(!is_multiline_macro_start("defineModel<string>()"));
}
#[test]
fn test_macro_call_line() {
assert!(is_macro_call_line(
"const layer = defineModel<Layer>('layer', { required: true });"
));
assert!(is_macro_call_line("defineExpose({})"));
assert!(!is_macro_call_line("import { defineModel } from 'vue'"));
assert!(!is_macro_call_line("const fx = FXS[layer.value.fxId];"));
assert!(!is_macro_call_line("const x = 'defineModel(test)'"));
}
#[test]
fn test_paren_macro_start() {
assert!(!is_paren_macro_start("defineExpose({})"));
assert!(!is_paren_macro_start("defineExpose({ foo: 'bar' })"));
assert!(is_paren_macro_start("defineExpose({"));
}
}