use crate::error::{DealerError, DealerResult};
pub(crate) struct TokenSpan {
pub(crate) fn count_leading_spaces_or_tabs(s: &str) -> usize {
pub(crate) fn find_dealer_block_range(
) -> DealerResult<Option<(usize, usize, usize)>> {
// Validate that every non-empty line (including comment-only lines) has no spaces in leading indentation
for (line_num, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
let first_non_ws = line.find(|c: char| !c.is_whitespace()).unwrap();
let leading = &line[..first_non_ws];
if leading.contains(' ') {
return Err(DealerError::Project(format!(
"line {}: invalid indentation: structural indentation must use tabs only, spaces are not allowed",
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("app ") || trimmed.starts_with("package ") {
let Some(d_idx) = decl_idx else {
let mut body_indent = None;
for line in &lines[d_idx + 1..] {
let code = split_code_and_comment(line).0;
if code.trim().is_empty() {
body_indent = Some(count_leading_spaces_or_tabs(line));
let b_indent = match body_indent {
let mut dealer_headers = Vec::new();
for (i, line) in lines.iter().enumerate().skip(d_idx + 1) {
let code = split_code_and_comment(line).0;
if code.trim().is_empty() {
let indent = count_leading_spaces_or_tabs(line);
let trimmed = code.trim();
if trimmed == "dealer" || (trimmed.starts_with("dealer ") && trimmed.contains("xtazy")) {
return Err(DealerError::Project(
"dealer block must be indented as a root child block".to_string(),
dealer_headers.push((i, indent));
if dealer_headers.is_empty() {
if dealer_headers.len() > 1 {
return Err(DealerError::Project(
"duplicate top-level dealer block found".to_string(),
let (h_idx, h_indent) = dealer_headers[0];
for (i, line) in lines.iter().enumerate().skip(h_idx + 1) {
let code = split_code_and_comment(line).0;
if code.trim().is_empty() {
let indent = count_leading_spaces_or_tabs(line);
Ok(Some((h_idx, h_idx + 1, end_idx)))
pub(crate) fn get_token_spans(s: &str) -> Vec<TokenSpan> {
let mut spans = Vec::new();
let mut current = String::new();
let mut start_idx = None;
let mut in_quotes = false;
for (idx, ch) in s.char_indices() {
if let Some(start) = start_idx {
} else if ch.is_whitespace() && !in_quotes {
if let Some(start) = start_idx {
if let Some(start) = start_idx.filter(|_| !current.is_empty()) {
pub(crate) fn split_code_and_comment(line: &str) -> (&str, Option<&str>) {
let mut in_quotes = false;
let mut comment_byte_idx = None;
let bytes = line.as_bytes();
for (char_idx, ch) in line.char_indices() {
|| (ch == '/' && char_idx + 1 < bytes.len() && bytes[char_idx + 1] == b'/'))
comment_byte_idx = Some(char_idx);
if let Some(idx) = comment_byte_idx {
(&line[..idx], Some(&line[idx..]))