use weavatrix_parse::{Token, TokenKind};
#[derive(Debug, Clone)]
pub struct Item {
pub start: usize,
pub end: usize,
pub text: String,
pub spread: bool,
}
#[derive(Debug, Clone)]
pub struct List {
pub items: Vec<Item>,
pub open: usize,
pub close: usize,
}
pub fn is_code(token: &Token) -> bool {
!matches!(
token.kind,
TokenKind::Whitespace
| TokenKind::Newline
| TokenKind::Indent
| TokenKind::LineComment
| TokenKind::BlockComment
| TokenKind::Unterminated
)
}
fn generic_end(source: &str, code: &[&Token], at: usize) -> Option<usize> {
const LOOKAHEAD: usize = 200;
let mut angle = 0_i32;
let mut nested = 0_i32;
let end = code.len().min(at + LOOKAHEAD);
for (offset, token) in code[at..end].iter().enumerate() {
match token.text(source) {
"<" => angle += 1,
">" => {
angle -= 1;
if angle == 0 && nested == 0 {
return Some(at + offset);
}
}
"(" | "[" | "{" => nested += 1,
")" | "]" | "}" => {
nested -= 1;
if nested < 0 {
return None;
}
}
";" | "=>" => return None,
_ => {}
}
}
None
}
fn item(source: &str, start: usize, end: usize) -> Item {
let text = source.get(start..end).unwrap_or_default().to_owned();
let head = text.trim_start();
Item {
spread: head.starts_with("...") || head.starts_with('*'),
start,
end,
text,
}
}
pub fn split(source: &str, code: &[&Token], open_index: usize) -> Option<List> {
if code.get(open_index)?.text(source) != "(" {
return None;
}
let open = code[open_index].start;
let mut items: Vec<Item> = Vec::new();
let mut depth = 0_i32;
let mut start: Option<usize> = None;
let mut end = open;
let mut at = open_index + 1;
while at < code.len() {
let token = code[at];
let text = token.text(source);
match text {
"(" | "[" | "{" => depth += 1,
")" | "]" | "}" if depth > 0 => depth -= 1,
")" => {
if let Some(from) = start {
items.push(item(source, from, end));
}
return Some(List {
items,
open,
close: token.start,
});
}
"," if depth == 0 => {
if let Some(from) = start {
items.push(item(source, from, end));
}
start = None;
at += 1;
continue;
}
"<" => {
if let Some(close) = generic_end(source, code, at) {
if start.is_none() {
start = Some(token.start);
}
end = code[close].end;
at = close + 1;
continue;
}
}
_ => {}
}
if start.is_none() {
start = Some(token.start);
}
end = token.end;
at += 1;
}
None
}
pub fn removal(items: &[Item], index: usize) -> Option<(usize, usize)> {
let item = items.get(index)?;
if let Some(next) = items.get(index + 1) {
Some((item.start, next.start))
} else if index > 0 {
Some((items[index - 1].end, item.end))
} else {
Some((item.start, item.end))
}
}
#[cfg(test)]
mod tests {
use super::{is_code, removal, split};
use weavatrix_parse::{Language, Token, tokenize};
fn parse(source: &str) -> super::List {
let tokens: Vec<Token> = tokenize(source, Language::TypeScript);
let code: Vec<&Token> = tokens.iter().filter(|token| is_code(token)).collect();
let open = code
.iter()
.position(|token| token.text(source) == "(")
.expect("an open parenthesis");
split(source, &code, open).expect("the list closes")
}
#[test]
fn an_empty_list_has_no_items() {
assert!(parse("f()").items.is_empty());
}
#[test]
fn items_carry_their_exact_bytes() {
let list = parse("f(alpha, beta)");
let texts: Vec<&str> = list.items.iter().map(|item| item.text.as_str()).collect();
assert_eq!(texts, ["alpha", "beta"]);
}
#[test]
fn a_comma_inside_a_generic_separates_nothing() {
let list = parse("f(map: Map<string, number>, flag: boolean)");
assert_eq!(list.items.len(), 2, "{:?}", list.items);
assert_eq!(list.items[0].text, "map: Map<string, number>");
}
#[test]
fn a_comma_inside_a_nested_call_separates_nothing() {
let list = parse("f(a = make(1, 2), b)");
assert_eq!(list.items.len(), 2, "{:?}", list.items);
}
#[test]
fn a_comma_inside_an_object_pattern_separates_nothing() {
let list = parse("f({ one, two }, rest)");
assert_eq!(list.items.len(), 2, "{:?}", list.items);
}
#[test]
fn a_comma_inside_a_string_separates_nothing() {
let list = parse("f('a, b', c)");
assert_eq!(list.items.len(), 2, "{:?}", list.items);
}
#[test]
fn a_spread_is_marked_rather_than_counted_as_ordinary() {
let list = parse("f(a, ...rest)");
assert!(!list.items[0].spread && list.items[1].spread);
}
#[test]
fn a_less_than_in_a_default_is_not_read_as_a_generic() {
let list = parse("f(a = x < 1 ? 2 : 3, b)");
assert_eq!(list.items.len(), 2, "{:?}", list.items);
}
#[test]
fn an_unclosed_list_is_refused_rather_than_guessed() {
let source = "f(a, b";
let tokens: Vec<Token> = tokenize(source, Language::TypeScript);
let code: Vec<&Token> = tokens.iter().filter(|token| is_code(token)).collect();
let open = code
.iter()
.position(|token| token.text(source) == "(")
.expect("open");
assert!(split(source, &code, open).is_none());
}
#[test]
fn removing_a_middle_item_takes_the_comma_after_it() {
let list = parse("f(a, b, c)");
let (start, end) = removal(&list.items, 1).expect("range");
assert_eq!(&"f(a, b, c)"[start..end], "b, ");
}
#[test]
fn removing_the_last_item_takes_the_comma_before_it() {
let list = parse("f(a, b)");
let (start, end) = removal(&list.items, 1).expect("range");
assert_eq!(&"f(a, b)"[start..end], ", b");
}
#[test]
fn removing_the_only_item_leaves_the_parentheses() {
let list = parse("f(a)");
let (start, end) = removal(&list.items, 0).expect("range");
assert_eq!(&"f(a)"[start..end], "a");
}
}