use crate::{FormatRule, ImportOrder};
use daml_parser::ast::{
Alt, ChoiceDecl, Decl, DoStmt, Expr, FieldAssign, GuardQualifier, Module, Span,
TemplateBodyDecl, TypeAnnotation,
};
use daml_parser::lexer::TriviaKind;
use daml_syntax::{SourceFile, SourceTokens};
use std::ops::{Add, AddAssign, Index};
const INDENT: i64 = 2;
const INDENT_WIDTH: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct ByteOffset(usize);
impl ByteOffset {
const fn new(value: usize) -> Self {
Self(value)
}
const fn get(self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct LineIndex(usize);
impl LineIndex {
const fn new(value: usize) -> Self {
Self(value)
}
const fn get(self) -> usize {
self.0
}
}
impl Add<usize> for LineIndex {
type Output = Self;
fn add(self, rhs: usize) -> Self::Output {
Self(self.0 + rhs)
}
}
impl AddAssign<usize> for LineIndex {
fn add_assign(&mut self, rhs: usize) {
self.0 += rhs;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct IndentDelta(i64);
impl IndentDelta {
const fn new(value: i64) -> Self {
Self(value)
}
const fn get(self) -> i64 {
self.0
}
const fn is_zero(self) -> bool {
self.0 == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LineStarts(Vec<usize>);
impl LineStarts {
fn get(&self, line: LineIndex) -> Option<&usize> {
self.0.get(line.get())
}
const fn len(&self) -> usize {
self.0.len()
}
fn iter(&self) -> impl Iterator<Item = (LineIndex, usize)> + '_ {
self.0
.iter()
.copied()
.enumerate()
.map(|(line, start)| (LineIndex::new(line), start))
}
}
impl Index<LineIndex> for LineStarts {
type Output = usize;
fn index(&self, index: LineIndex) -> &Self::Output {
&self.0[index.get()]
}
}
const MAX_STRUCTURAL_PASSES: usize = 6;
#[must_use]
pub fn format_ast(src: &str, options: crate::FormatOptions) -> String {
if has_source_location_expectation(src) || has_trailing_with_comment(src) {
return src.to_string();
}
let rules = options.rules();
let mut base = if rules.contains(FormatRule::Layout) {
run_structural_passes(src)
} else {
src.to_string()
};
if rules.contains(FormatRule::Imports) && options.import_order() == ImportOrder::Organize {
base = organize_imports(&base);
}
if rules.contains(FormatRule::SyntaxNormalization) {
base = rewrite_syntax_forms(&base, rules.contains(FormatRule::Layout));
if rules.contains(FormatRule::Layout) {
base = run_structural_passes(&base);
}
} else if rules.contains(FormatRule::Layout) {
base = rewrite_pure_layout_forms(&base);
base = run_structural_passes(&base);
}
if rules.contains(FormatRule::Spacing) {
let full = crate::normalize_gaps(&base, crate::ColonSpacingMode::Canonical);
if same_tokens(&base, &full) {
return full;
}
let ws_only = crate::normalize_gaps(&base, crate::ColonSpacingMode::Preserve);
if same_tokens(&base, &ws_only) {
return ws_only;
}
}
base
}
fn run_structural_passes(src: &str) -> String {
let mut base = src.to_string();
for _ in 0..MAX_STRUCTURAL_PASSES {
let mut next = base.clone();
next = gated_module_pass(&next);
next = gated_template_pass(&next);
next = gated_choice_pass(&next);
next = gated_type_def_pass(&next);
next = gated_guard_pass(&next);
next = gated_record_update_pass(&next);
next = gated_try_pass(&next);
next = gated_continuation_pass(&next);
next = gated_do_pass(&next);
next = gated_if_pass(&next);
next = gated_case_pass(&next);
next = gated_letin_pass(&next);
next = gated_con_with_pass(&next);
if next == base {
break;
}
base = next;
}
base
}
fn gated_do_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_do_blocks(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_if_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_ifs(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_case_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_cases(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_letin_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_letins(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_con_with_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_con_with(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_template_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_templates(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_module_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_modules_and_imports(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_choice_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_choices(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_type_def_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_type_defs(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_guard_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_guards(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_record_update_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_record_updates(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_try_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_tries(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
fn gated_continuation_pass(src: &str) -> String {
let source_file = SourceFile::parse(src);
let r = reindent_continuations(src, source_file.module());
if r != src && same_tokens(src, &r) {
r
} else {
src.to_string()
}
}
#[must_use]
pub fn coverage(src: &str) -> crate::FormatCoverage {
let source_file = SourceFile::parse(src);
let module = source_file.module();
let formatted = do_block_edits(src, module).len()
+ module_edits(src, module).len()
+ choice_edits(src, module).len()
+ type_def_edits(src, module).len()
+ guard_edits(src, module).len()
+ record_update_edits(src, module).len()
+ try_edits(src, module).len()
+ continuation_edits(src, module).len()
+ if_edits(src, module).len()
+ case_edits(src, module).len()
+ letin_edits(src, module).len()
+ con_with_edits(src, module).len()
+ template_edits(src, module).len();
crate::FormatCoverage {
edit_candidates: formatted,
modeled_constructs: modeled_construct_count(module),
}
}
fn modeled_construct_count(module: &Module) -> usize {
let mut count = 0usize;
walk_module_expressions(module, &mut |e| match e {
Expr::Do { .. } | Expr::If { .. } | Expr::Case { .. } | Expr::LetIn { .. } => count += 1,
Expr::Record { base, fields, .. }
if matches!(base.as_ref(), Expr::Con { .. }) && !fields.is_empty() =>
{
count += 1
}
_ => {}
});
for d in &module.decls {
match d {
Decl::Template(t) if !t.fields.is_empty() || !t.body.is_empty() => count += 1,
Decl::Interface(i) if !i.methods.is_empty() || !i.choices.is_empty() => count += 1,
Decl::TypeDef { .. } => count += 1,
_ => {}
}
}
count += module.imports.iter().filter(|i| !i.span.is_empty()).count();
count
}
fn same_tokens(a: &str, b: &str) -> bool {
let a = SourceTokens::lex(a);
let b = SourceTokens::lex(b);
let la = a.laid_out_tokens();
let lb = b.laid_out_tokens();
la.len() == lb.len() && la.iter().zip(lb).all(|(x, y)| x.kind() == y.kind())
}
fn has_source_location_expectation(src: &str) -> bool {
src.lines()
.filter(|line| line.trim_start().starts_with("-- @"))
.any(|line| {
line.contains("range=")
|| line.contains(".location")
|| line.contains("start_line")
|| line.contains("start_col")
|| line.contains("end_line")
|| line.contains("end_col")
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Replacement {
start: usize,
end: usize,
text: String,
}
fn apply_replacements(src: &str, replacements: &[Replacement]) -> String {
if replacements.is_empty() {
return src.to_string();
}
let mut ordered = replacements.to_vec();
ordered.sort_by_key(|r| (r.start, r.end));
let mut out = String::with_capacity(src.len());
let mut cursor = 0usize;
for r in ordered {
if r.start < cursor || r.start > r.end || r.end > src.len() {
return src.to_string();
}
out.push_str(&src[cursor..r.start]);
out.push_str(&r.text);
cursor = r.end;
}
out.push_str(&src[cursor..]);
out
}
fn rewrite_syntax_forms(src: &str, include_pure_layout: bool) -> String {
let mut base = rewrite_line_forms(src);
if include_pure_layout {
base = rewrite_pure_layout_forms(&base);
}
let source_file = SourceFile::parse(&base);
let mut replacements = Vec::new();
collect_inline_expression_rewrites(&base, source_file.module(), &mut replacements);
apply_replacements(&base, &replacements)
}
fn rewrite_pure_layout_forms(src: &str) -> String {
let base = rewrite_line_infix_continuations(src);
let base = rewrite_lambda_bodies(&base);
rewrite_infix_continuations(&base)
}
fn rewrite_line_infix_continuations(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let mut last_expr_indent: Option<usize> = None;
for line in src.split_inclusive('\n') {
let (body, ending) = split_line_ending(line);
let leading = body.len() - body.trim_start_matches(' ').len();
let trimmed = body[leading..].trim_end();
if trimmed.is_empty() || trimmed.starts_with("--") {
out.push_str(line);
continue;
}
if let Some(comment_at) = body.find("--") {
if !body[..comment_at].trim().is_empty() {
out.push_str(line);
continue;
}
}
if starts_with_infix_operator(trimmed) {
if let Some(prev) = last_expr_indent {
let target = prev.saturating_add(INDENT_WIDTH);
if leading != target {
out.push_str(&" ".repeat(target));
out.push_str(trimmed);
out.push_str(ending);
continue;
}
}
}
out.push_str(line);
if !starts_with_infix_operator(trimmed)
&& !starts_with_word(trimmed, "module")
&& !starts_with_word(trimmed, "import")
&& !trimmed.ends_with(':')
{
last_expr_indent = Some(leading);
}
}
out
}
fn rewrite_line_forms(src: &str) -> String {
let mut out = String::with_capacity(src.len());
for line in src.split_inclusive('\n') {
let (body, ending) = split_line_ending(line);
let leading = body.len() - body.trim_start_matches(' ').len();
let trimmed = body[leading..].trim_end();
if trimmed.is_empty() || trimmed.starts_with("--") {
out.push_str(line);
continue;
}
if let Some(comment_at) = body.find("--") {
if !body[..comment_at].trim().is_empty() {
out.push_str(line);
continue;
}
}
if let Some(rewritten) = rewrite_signature_line(body, ending) {
out.push_str(&rewritten);
continue;
}
if let Some(rewritten) = rewrite_inline_let_line(body, ending) {
out.push_str(&rewritten);
continue;
}
if let Some(rewritten) = rewrite_long_application_line(body, ending) {
out.push_str(&rewritten);
continue;
}
out.push_str(line);
}
out
}
fn split_line_ending(line: &str) -> (&str, &str) {
line.strip_suffix("\r\n").map_or_else(
|| {
line.strip_suffix('\n')
.map_or((line, ""), |body| (body, "\n"))
},
|body| (body, "\r\n"),
)
}
fn rewrite_signature_line(body: &str, ending: &str) -> Option<String> {
let leading = body.len() - body.trim_start_matches(' ').len();
let trimmed = body[leading..].trim_end();
let colon = trimmed.find(':')?;
if trimmed[..colon].contains('=') {
return None;
}
let name = trimmed[..colon].trim();
if name.is_empty() || name.contains(' ') {
return None;
}
let ty = trimmed[colon + 1..].trim();
let arrow_count = ty.matches("->").count();
if arrow_count < 3 && trimmed.chars().count() <= 80 {
return None;
}
let indent = " ".repeat(leading);
let parts = split_top_level_arrows(ty)?;
if parts.len() < 2 {
return None;
}
let mut out = format!("{indent}{name}:");
for (idx, part) in parts.iter().enumerate() {
out.push_str(ending);
out.push_str(&indent);
out.push_str(" ");
if idx > 0 {
out.push_str("-> ");
}
out.push_str(part);
}
out.push_str(ending);
Some(out)
}
fn split_top_level_arrows(ty: &str) -> Option<Vec<&str>> {
let bytes = ty.as_bytes();
let mut parts = Vec::new();
let mut start = 0usize;
let mut depth = 0i32;
let mut i = 0usize;
while i + 1 < bytes.len() {
match bytes[i] {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b'-' if bytes[i + 1] == b'>' && depth == 0 => {
let part = ty[start..i].trim();
if part.is_empty() {
return None;
}
parts.push(part);
i += 2;
start = i;
continue;
}
_ => {}
}
if depth < 0 {
return None;
}
i += 1;
}
let part = ty[start..].trim();
if part.is_empty() {
return None;
}
parts.push(part);
Some(parts)
}
fn rewrite_inline_let_line(body: &str, ending: &str) -> Option<String> {
let leading = body.len() - body.trim_start_matches(' ').len();
let trimmed = body[leading..].trim_end();
let marker = " = let ";
let let_at = trimmed.find(marker)? + " = ".len();
let prefix = trimmed[..let_at].trim_end();
let rest = &trimmed[let_at + "let ".len()..];
let in_at = rest.rfind(" in ")?;
let bindings = &rest[..in_at];
let body_expr = rest[in_at + " in ".len()..].trim();
if !bindings.contains(';') {
return None;
}
let indent = " ".repeat(leading.saturating_add(INDENT_WIDTH));
let nested = " ".repeat(leading.saturating_add(2 * INDENT_WIDTH));
let mut out = format!("{}{}{}", " ".repeat(leading), prefix, ending);
out.push_str(&indent);
out.push_str("let");
out.push_str(ending);
for binding in bindings.split(';').map(str::trim).filter(|b| !b.is_empty()) {
out.push_str(&nested);
out.push_str(binding);
out.push_str(ending);
}
out.push_str(&indent);
out.push_str("in ");
out.push_str(body_expr);
out.push_str(ending);
Some(out)
}
fn rewrite_long_application_line(body: &str, ending: &str) -> Option<String> {
let leading = body.len() - body.trim_start_matches(' ').len();
let trimmed = body[leading..].trim_end();
let marker = " = ";
let eq_at = trimmed.find(marker)?;
let prefix = trimmed[..eq_at + marker.len() - 1].trim_end();
let rhs = trimmed[eq_at + marker.len()..].trim();
if rhs.contains('"')
|| rhs.chars().any(|c| {
matches!(
c,
'+' | '*'
| '/'
| '<'
| '>'
| '='
| ';'
| '\\'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
| ','
)
})
{
return None;
}
if rhs.chars().any(|c| matches!(c, '\''))
&& rhs.split_whitespace().any(|part| part.starts_with('\''))
{
return None;
}
let parts: Vec<_> = rhs.split_whitespace().collect();
if parts.len() < 7 {
return None;
}
if parts[0]
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
{
return None;
}
let indent = " ".repeat(leading.saturating_add(INDENT_WIDTH));
let nested = " ".repeat(leading.saturating_add(2 * INDENT_WIDTH));
let mut out = format!("{}{}{}", " ".repeat(leading), prefix, ending);
out.push_str(&indent);
out.push_str(parts[0]);
out.push_str(ending);
for part in parts.iter().skip(1) {
out.push_str(&nested);
out.push_str(part);
out.push_str(ending);
}
Some(out)
}
fn collect_inline_expression_rewrites(
src: &str,
module: &Module,
replacements: &mut Vec<Replacement>,
) {
for decl in &module.decls {
let Decl::Function(fun) = decl else {
continue;
};
for eq in &fun.equations {
let line_starts = line_start_table(src);
let eq_line = line_of(&line_starts, ByteOffset::new(eq.span.start_usize()));
if leading_has_tab(src, line_starts[eq_line]) {
continue;
}
let body_indent =
indent_of_usize(src, &line_starts, eq_line).saturating_add(INDENT_WIDTH);
collect_expr_rewrite(
src,
&eq.body,
body_indent,
RewriteLeadMode::LeadCandidate,
replacements,
);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RewriteLeadMode {
LeadCandidate,
InlineOnly,
}
fn case_alts_are_simple_inline(alts: &[Alt]) -> bool {
alts.iter().all(|alt| {
alt.branches.len() == 1
&& alt.branches[0].guards.is_empty()
&& alt.where_bindings.is_empty()
})
}
fn collect_case_alt_rewrites(
src: &str,
alts: &[Alt],
indent: usize,
rewrite_mode: RewriteLeadMode,
replacements: &mut Vec<Replacement>,
) {
for alt in alts {
for branch in &alt.branches {
for guard in &branch.guards {
match guard {
GuardQualifier::Bool { expr, .. } | GuardQualifier::Pattern { expr, .. } => {
collect_expr_rewrite(src, expr, indent, rewrite_mode, replacements)
}
_ => {}
}
}
collect_expr_rewrite(src, &branch.body, indent, rewrite_mode, replacements);
}
for wb in &alt.where_bindings {
collect_expr_rewrite(src, &wb.expr, indent, rewrite_mode, replacements);
}
}
}
fn collect_expr_rewrite(
src: &str,
expr: &Expr,
indent: usize,
rewrite_mode: RewriteLeadMode,
replacements: &mut Vec<Replacement>,
) {
let span = expr.span();
let line_starts = line_start_table(src);
match expr {
Expr::If {
cond,
then_branch,
else_branch,
..
} if rewrite_mode == RewriteLeadMode::LeadCandidate
&& same_line_span(src, span)
&& inline_if_parts_are_simple(src, cond, then_branch, else_branch) =>
{
let ind = " ".repeat(indent);
let nested = " ".repeat(indent.saturating_add(INDENT_WIDTH));
let mut text = String::new();
if rewrite_mode == RewriteLeadMode::LeadCandidate {
text.push('\n');
text.push_str(&ind);
}
text.push_str("if ");
text.push_str(src[cond.span().range()].trim());
text.push('\n');
text.push_str(&nested);
text.push_str("then ");
text.push_str(src[then_branch.span().range()].trim());
text.push('\n');
text.push_str(&nested);
text.push_str("else ");
text.push_str(src[else_branch.span().range()].trim());
replacements.push(Replacement {
start: span.start_usize(),
end: span.end_usize(),
text,
});
}
Expr::Case {
scrutinee, alts, ..
} if rewrite_mode == RewriteLeadMode::LeadCandidate
&& same_line_span(src, span)
&& !alts.is_empty()
&& case_alts_are_simple_inline(alts) =>
{
let ind = " ".repeat(indent);
let mut text = String::from("case ");
text.push_str(src[scrutinee.span().range()].trim());
text.push_str(" of");
for alt in alts {
text.push('\n');
text.push_str(&ind);
text.push_str(src[alt.pat.span().range()].trim());
text.push_str(" -> ");
text.push_str(src[alt.body.span().range()].trim());
}
replacements.push(Replacement {
start: span.start_usize(),
end: span.end_usize(),
text,
});
}
Expr::LetIn { bindings, body, .. }
if rewrite_mode == RewriteLeadMode::LeadCandidate
&& same_line_span(src, span)
&& !bindings.is_empty() =>
{
let ind = " ".repeat(indent);
let nested = " ".repeat(indent.saturating_add(INDENT_WIDTH));
let mut text = String::new();
if rewrite_mode == RewriteLeadMode::LeadCandidate {
text.push('\n');
text.push_str(&ind);
}
text.push_str("let");
for binding in bindings {
text.push('\n');
text.push_str(&nested);
text.push_str(src[binding.span.range()].trim());
}
text.push('\n');
text.push_str(&ind);
text.push_str("in ");
text.push_str(src[body.span().range()].trim());
replacements.push(Replacement {
start: span.start_usize(),
end: span.end_usize(),
text,
});
}
Expr::Record { base, fields, .. }
if rewrite_mode == RewriteLeadMode::LeadCandidate
&& same_line_span(src, span)
&& src[span.range()].contains(';')
&& matches!(base.as_ref(), Expr::Con { .. })
&& fields.len() > 1 =>
{
let ind = " ".repeat(indent);
let mut text = String::new();
text.push_str(src[base.span().range()].trim());
text.push_str(" with");
for field in fields {
text.push('\n');
text.push_str(&ind);
text.push_str(src[field.span().range()].trim());
}
replacements.push(Replacement {
start: span.start_usize(),
end: span.end_usize(),
text,
});
}
Expr::App { func, args, .. }
if rewrite_mode == RewriteLeadMode::LeadCandidate
&& same_line_span(src, span)
&& args.len() >= 6
&& root_app_func(func).is_some()
&& app_args_are_simple(src, args) =>
{
let ind = " ".repeat(indent);
let nested = " ".repeat(indent.saturating_add(INDENT_WIDTH));
let mut text = String::new();
if rewrite_mode == RewriteLeadMode::LeadCandidate {
text.push('\n');
text.push_str(&ind);
}
text.push_str(src[func.span().range()].trim());
for arg in args {
text.push('\n');
text.push_str(&nested);
text.push_str(src[arg.span().range()].trim());
}
replacements.push(Replacement {
start: span.start_usize(),
end: span.end_usize(),
text,
});
}
_ => {
let expr_line = line_of(&line_starts, ByteOffset::new(span.start_usize()));
let child_indent = if expr_line.get() < line_starts.len() {
indent_of_usize(src, &line_starts, expr_line).saturating_add(INDENT_WIDTH)
} else {
indent.saturating_add(INDENT_WIDTH)
};
match expr {
Expr::App { func, args, .. } => {
collect_expr_rewrite(
src,
func,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
for arg in args {
collect_expr_rewrite(
src,
arg,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
}
Expr::BinOp { lhs, rhs, .. } => {
collect_expr_rewrite(
src,
lhs,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
collect_expr_rewrite(
src,
rhs,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
Expr::If {
cond,
then_branch,
else_branch,
..
} => {
collect_expr_rewrite(
src,
cond,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
collect_expr_rewrite(
src,
then_branch,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
collect_expr_rewrite(
src,
else_branch,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
Expr::Case {
scrutinee, alts, ..
} => {
collect_expr_rewrite(
src,
scrutinee,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
collect_case_alt_rewrites(
src,
alts,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
Expr::LetIn { bindings, body, .. } => {
for binding in bindings {
collect_expr_rewrite(
src,
&binding.expr,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
collect_expr_rewrite(
src,
body,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
Expr::Record { base, fields, .. } => {
collect_expr_rewrite(
src,
base,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
for field in fields {
if let FieldAssign::Assign { value, .. } = field {
collect_expr_rewrite(
src,
value,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
}
}
Expr::Lambda { body, .. } | Expr::Neg { expr: body, .. } => {
collect_expr_rewrite(
src,
body,
child_indent,
RewriteLeadMode::InlineOnly,
replacements,
);
}
_ => {}
}
}
}
}
fn root_app_func(expr: &Expr) -> Option<()> {
matches!(expr, Expr::Var { .. }).then_some(())
}
fn inline_if_parts_are_simple(
src: &str,
cond: &Expr,
then_branch: &Expr,
else_branch: &Expr,
) -> bool {
[cond.span(), then_branch.span(), else_branch.span()]
.into_iter()
.map(|span| src[span.range()].trim())
.all(is_simple_inline_piece)
}
fn is_simple_inline_piece(text: &str) -> bool {
!text.is_empty()
&& !text.contains('\n')
&& !text
.chars()
.any(|c| matches!(c, '(' | ')' | '{' | '}' | '[' | ']' | ';' | ','))
}
fn app_args_are_simple(src: &str, args: &[Expr]) -> bool {
args.iter()
.map(|arg| src[arg.span().range()].trim())
.all(is_simple_app_arg)
}
fn is_simple_app_arg(text: &str) -> bool {
!text.is_empty()
&& !text.contains('\n')
&& text
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '\'' | '.' | '-'))
}
fn same_line_span(src: &str, span: Span) -> bool {
!src[span.range()].contains('\n')
}
fn has_trailing_with_comment(src: &str) -> bool {
src.lines().any(|line| {
let Some(comment_at) = line.find("--") else {
return false;
};
line[..comment_at]
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '\''))
.any(|word| word == "with")
})
}
#[derive(Clone, Copy)]
struct BorrowedImport<'a> {
group: ImportGroup,
module_name: &'a str,
text: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ImportGroup {
DamlStdlib,
DaLibrary,
LocalOrExternal,
}
fn organize_imports(src: &str) -> String {
let source_file = SourceFile::parse(src);
let module = source_file.module();
if module.imports.len() < 2 {
return src.to_string();
}
let Some(first) = module.imports.first() else {
return src.to_string();
};
let Some(last) = module.imports.last() else {
return src.to_string();
};
let line_starts = line_start_table(src);
let start_line = line_of(&line_starts, ByteOffset::new(first.span.start_usize()));
let end_line = line_of(
&line_starts,
ByteOffset::new(last.span.end_usize().saturating_sub(1)),
);
let block_start = line_starts[start_line];
let block_end = *line_starts.get(end_line + 1).unwrap_or(&src.len());
if src[block_start..block_end].contains("--")
|| src[block_start..block_end].contains("{-")
|| src[block_start..block_end]
.lines()
.any(|line| line.trim_start().starts_with('#'))
{
return src.to_string();
}
let mut imports: Vec<BorrowedImport<'_>> = module
.imports
.iter()
.map(|imp| BorrowedImport {
group: import_group(&imp.module_name),
module_name: &imp.module_name,
text: src[imp.span.range()].trim(),
})
.collect();
imports.sort_by(|a, b| {
a.group
.cmp(&b.group)
.then_with(|| a.module_name.cmp(b.module_name))
.then_with(|| a.text.cmp(b.text))
});
let order_unchanged = module
.imports
.iter()
.zip(&imports)
.all(|(imp, sorted)| sorted.text == src[imp.span.range()].trim());
if order_unchanged {
return src.to_string();
}
let mut text = String::new();
let mut prev_group = None;
for entry in imports {
if prev_group.is_some_and(|g| g != entry.group) {
text.push('\n');
}
text.push_str(entry.text);
text.push('\n');
prev_group = Some(entry.group);
}
let mut out = String::with_capacity(src.len());
out.push_str(&src[..block_start]);
out.push_str(&text);
out.push_str(&src[block_end..]);
out
}
fn import_group(module_name: &str) -> ImportGroup {
if module_name.starts_with("Daml.") {
ImportGroup::DamlStdlib
} else if module_name.starts_with("DA.") {
ImportGroup::DaLibrary
} else {
ImportGroup::LocalOrExternal
}
}
fn rewrite_lambda_bodies(src: &str) -> String {
let source_file = SourceFile::parse(src);
let module = source_file.module();
let line_starts = line_start_table(src);
let mut edits = Vec::new();
walk_module_expressions(module, &mut |expr| {
let Expr::Lambda { span, body, .. } = expr else {
return;
};
let lambda_line = line_of(&line_starts, ByteOffset::new(span.start_usize()));
let body_line = line_of(&line_starts, ByteOffset::new(body.span().start_usize()));
if body_line <= lambda_line || leading_has_tab(src, line_starts[body_line]) {
return;
}
let target = indent_of(src, &line_starts, lambda_line) + INDENT;
let delta = target - indent_of(src, &line_starts, body_line);
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(line_starts[body_line]),
block_end: ByteOffset::new(body.span().end_usize()),
delta: IndentDelta::new(delta),
});
}
});
if edits.is_empty() {
return src.to_string();
}
let shifted = apply_shifts(src, &edits);
if same_tokens(src, &shifted) {
shifted
} else {
src.to_string()
}
}
fn rewrite_infix_continuations(src: &str) -> String {
let source_file = SourceFile::parse(src);
let module = source_file.module();
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for decl in &module.decls {
let Decl::Function(fun) = decl else {
continue;
};
for eq in &fun.equations {
let body_span = eq.body.span();
let first_line = line_of(&line_starts, ByteOffset::new(body_span.start_usize()));
let target = indent_of(src, &line_starts, first_line) + INDENT;
let mut line = first_line + 1;
while line.get() < line_starts.len() && line_starts[line] < body_span.end_usize() {
let Some(trimmed) = code_line_trimmed(src, &line_starts, &comments, line) else {
line += 1;
continue;
};
if starts_with_infix_operator(trimmed) {
push_code_line_edit(&mut edits, src, &line_starts, &comments, line, target);
}
line += 1;
}
}
}
if edits.is_empty() {
return src.to_string();
}
let shifted = apply_shifts(src, &edits);
if same_tokens(src, &shifted) {
shifted
} else {
src.to_string()
}
}
fn starts_with_infix_operator(trimmed: &str) -> bool {
if trimmed.starts_with("->")
|| trimmed == ":"
|| trimmed
.strip_prefix('|')
.is_some_and(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
{
return false;
}
trimmed
.chars()
.next()
.is_some_and(|c| matches!(c, '&' | '|' | '+' | '-' | '*' | '/' | '<' | '>' | '=' | ':'))
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Edit {
child_start: ByteOffset,
block_end: ByteOffset,
delta: IndentDelta,
}
fn reindent_do_blocks(src: &str, module: &Module) -> String {
let edits = do_block_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn do_block_edits(src: &str, module: &Module) -> Vec<Edit> {
let mut do_block_spans: Vec<Span> = Vec::new();
collect_do_block_spans(module, &mut do_block_spans);
do_block_spans.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for do_span in do_block_spans {
if accepted.iter().any(|a| {
a.start_usize() <= do_span.start_usize()
&& do_span.end_usize() <= a.end_usize()
&& *a != do_span
}) {
continue;
}
let do_line = line_of(&line_starts, ByteOffset::new(do_span.start_usize()));
let do_indent = indent_of(src, &line_starts, do_line);
let Some(first_stmt_line) =
first_code_line_after(src, &line_starts, &comments, do_line, do_span.end_usize())
else {
continue; };
accepted.push(do_span);
if leading_has_tab(src, line_starts[first_stmt_line]) {
continue;
}
let cur = indent_of(src, &line_starts, first_stmt_line);
let delta = (do_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(line_starts[first_stmt_line]),
block_end: ByteOffset::new(do_span.end_usize()),
delta: IndentDelta::new(delta),
});
}
}
edits
}
fn apply_shifts(src: &str, edits: &[Edit]) -> String {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut out = String::with_capacity(src.len());
for (li, ls) in line_starts.iter() {
let le = *line_starts.get(li + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let trimmed = line.trim_start_matches(' ');
let cur = line.len() - trimmed.len();
let content_byte = ByteOffset::new(ls + cur);
let delta = edits
.iter()
.find(|e| e.child_start <= content_byte && content_byte < e.block_end)
.map(|e| e.delta)
.unwrap_or(IndentDelta::new(0));
if delta.is_zero()
|| line.trim().is_empty()
|| is_comment_line(&comments, content_byte.get())
|| leading_has_tab(src, ls)
{
out.push_str(line);
continue;
}
let new = add_signed_to_usize_saturating(cur, delta.get());
out.push_str(&" ".repeat(new));
out.push_str(&line[cur..]);
}
out
}
fn comment_spans(src: &str) -> Vec<(usize, usize)> {
let source_tokens = SourceTokens::lex(src);
let mut v: Vec<(usize, usize)> = source_tokens
.trivia()
.iter()
.filter(|t| matches!(t.kind(), TriviaKind::LineComment | TriviaKind::BlockComment))
.map(|t| (t.start().get(), t.end().get()))
.collect();
v.sort_by_key(|&(s, _)| s);
v
}
fn is_comment_line(comments: &[(usize, usize)], content_byte: usize) -> bool {
comments
.iter()
.any(|&(s, e)| s <= content_byte && content_byte < e)
}
fn line_start_table(src: &str) -> LineStarts {
LineStarts(
std::iter::once(0)
.chain(src.match_indices('\n').map(|(i, _)| i + 1))
.collect(),
)
}
fn line_of(line_starts: &LineStarts, byte: ByteOffset) -> LineIndex {
match line_starts.0.binary_search(&byte.get()) {
Ok(i) => LineIndex::new(i),
Err(i) => LineIndex::new(i - 1),
}
}
fn indent_of_usize(src: &str, line_starts: &LineStarts, line: LineIndex) -> usize {
src[line_starts[line]..]
.chars()
.take_while(|&c| c == ' ')
.count()
}
fn indent_of(src: &str, line_starts: &LineStarts, line: LineIndex) -> i64 {
usize_to_i64_saturating(indent_of_usize(src, line_starts, line))
}
fn usize_to_i64_saturating(value: usize) -> i64 {
i64::try_from(value).unwrap_or(i64::MAX)
}
fn add_signed_to_usize_saturating(value: usize, delta: i64) -> usize {
if delta >= 0 {
value.saturating_add(usize::try_from(delta).unwrap_or(usize::MAX))
} else {
value.saturating_sub(usize::try_from(delta.unsigned_abs()).unwrap_or(usize::MAX))
}
}
fn leading_has_tab(src: &str, line_start: usize) -> bool {
src[line_start..]
.chars()
.take_while(|&c| c == ' ' || c == '\t')
.any(|c| c == '\t')
}
fn first_code_line_after(
src: &str,
line_starts: &LineStarts,
comments: &[(usize, usize)],
do_line: LineIndex,
block_end: usize,
) -> Option<LineIndex> {
let mut l = do_line + 1;
while l.get() < line_starts.len() && line_starts[l] < block_end {
let ls = line_starts[l];
let le = *line_starts.get(l + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let cur = line.len() - line.trim_start_matches(' ').len();
if !line.trim().is_empty() && !is_comment_line(comments, ls + cur) {
return Some(l);
}
l += 1;
}
None
}
fn next_code_line_starts_with_keyword(
src: &str,
line_starts: &LineStarts,
comments: &[(usize, usize)],
after_line: LineIndex,
keyword: &str,
) -> Option<LineIndex> {
let mut l = after_line + 1;
while l.get() < line_starts.len() {
let ls = line_starts[l];
let le = *line_starts.get(l + 1).unwrap_or(&src.len());
let line = &src[ls..le];
let cur = line.len() - line.trim_start_matches(' ').len();
let trimmed = line[cur..].trim_end();
if line.trim().is_empty() || is_comment_line(comments, ls + cur) {
l += 1;
continue;
}
return trimmed
.strip_prefix(keyword)
.is_some_and(|rest| {
rest.is_empty()
|| rest
.chars()
.next()
.is_some_and(|c| !c.is_ascii_alphanumeric() && c != '_')
})
.then_some(l);
}
None
}
fn collect_do_block_spans(module: &Module, do_block_spans: &mut Vec<Span>) {
walk_module_expressions(module, &mut |expr| {
if let Expr::Do { span, .. } = expr {
do_block_spans.push(*span);
}
});
}
fn walk_module_expressions(module: &Module, f: &mut impl FnMut(&Expr)) {
for decl in &module.decls {
match decl {
Decl::Function(fun) => {
for eq in &fun.equations {
walk_expression(&eq.body, f);
for (g, b) in &eq.guards {
walk_expression(g, f);
walk_expression(b, f);
}
for wb in &eq.where_bindings {
walk_expression(&wb.expr, f);
}
}
}
Decl::Template(t) => {
for b in &t.body {
match b {
TemplateBodyDecl::Choice(c) => {
if let Some(body) = &c.body {
walk_expression(body, f);
}
}
TemplateBodyDecl::Ensure { expr, .. }
| TemplateBodyDecl::Key { expr, .. }
| TemplateBodyDecl::Maintainer { expr, .. } => walk_expression(expr, f),
_ => {}
}
}
}
_ => {}
}
}
}
fn walk_expression(expr: &Expr, f: &mut impl FnMut(&Expr)) {
f(expr);
match expr {
Expr::App { func, args, .. } => {
walk_expression(func, f);
args.iter().for_each(|arg| walk_expression(arg, f));
}
Expr::BinOp { lhs, rhs, .. } => {
walk_expression(lhs, f);
walk_expression(rhs, f);
}
Expr::Neg { expr, .. } | Expr::Lambda { body: expr, .. } => walk_expression(expr, f),
Expr::If {
cond,
then_branch,
else_branch,
..
} => {
walk_expression(cond, f);
walk_expression(then_branch, f);
walk_expression(else_branch, f);
}
Expr::Case {
scrutinee, alts, ..
} => {
walk_expression(scrutinee, f);
for alt in alts {
for branch in &alt.branches {
for guard in &branch.guards {
match guard {
GuardQualifier::Bool { expr, .. }
| GuardQualifier::Pattern { expr, .. } => walk_expression(expr, f),
_ => {}
}
}
walk_expression(&branch.body, f);
}
for wb in &alt.where_bindings {
walk_expression(&wb.expr, f);
}
}
}
Expr::Do { stmts, .. } => {
for s in stmts {
match s {
DoStmt::Bind { expr, .. } | DoStmt::Expr { expr, .. } => {
walk_expression(expr, f)
}
DoStmt::Let { bindings, .. } => {
bindings.iter().for_each(|b| walk_expression(&b.expr, f))
}
_ => {}
}
}
}
Expr::LetIn { bindings, body, .. } => {
bindings.iter().for_each(|b| walk_expression(&b.expr, f));
walk_expression(body, f);
}
Expr::Record { base, fields, .. } => {
walk_expression(base, f);
for fa in fields {
if let FieldAssign::Assign { value, .. } = fa {
walk_expression(value, f);
}
}
}
Expr::Tuple { items, .. } | Expr::List { items, .. } => {
items.iter().for_each(|item| walk_expression(item, f))
}
Expr::Try { body, handlers, .. } => {
walk_expression(body, f);
for handler in handlers {
for branch in &handler.branches {
for guard in &branch.guards {
match guard {
GuardQualifier::Bool { expr, .. }
| GuardQualifier::Pattern { expr, .. } => walk_expression(expr, f),
_ => {}
}
}
walk_expression(&branch.body, f);
}
for wb in &handler.where_bindings {
walk_expression(&wb.expr, f);
}
}
}
Expr::LeftSection { operand, .. } | Expr::RightSection { operand, .. } => {
walk_expression(operand, f);
}
_ => {}
}
}
fn find_keyword(
src: &str,
from: usize,
to: usize,
kw: &str,
comments: &[(usize, usize)],
) -> Option<usize> {
let hay = &src[from..to.min(src.len())];
let bytes = hay.as_bytes();
let mut i = 0;
while let Some(rel) = hay[i..].find(kw) {
let at = i + rel;
let before_ok = at == 0 || !is_ident_byte(bytes[at - 1]);
let after = at + kw.len();
let after_ok = after >= hay.len() || !is_ident_byte(bytes[after]);
let abs = from + at;
if before_ok && after_ok && !is_comment_line(comments, abs) {
return Some(abs);
}
i = at + 1;
}
None
}
fn if_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut ifs: Vec<(Span, usize, usize, Span, Span)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::If {
span,
cond,
then_branch,
else_branch,
..
} = e
{
ifs.push((
*span,
span.start_usize(),
cond.span().end_usize(),
then_branch.span(),
else_branch.span(),
));
}
});
ifs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (if_span, if_byte, cond_end, then_span, else_span) in ifs {
if accepted.iter().any(|a| {
a.start_usize() <= if_span.start_usize()
&& if_span.end_usize() <= a.end_usize()
&& *a != if_span
}) {
continue;
}
accepted.push(if_span);
let if_line = line_of(&line_starts, ByteOffset::new(if_byte));
if leading_has_tab(src, line_starts[if_line]) {
continue;
}
let if_col = usize_to_i64_saturating(src[line_starts[if_line]..if_byte].chars().count());
let target = if_col + INDENT;
let then_byte = find_keyword(src, cond_end, then_span.start_usize(), "then", &comments);
let else_byte = find_keyword(
src,
then_span.end_usize(),
else_span.start_usize(),
"else",
&comments,
);
for (kw_byte, branch_end) in [
(then_byte, then_span.end_usize()),
(else_byte, else_span.end_usize()),
] {
let Some(kw_byte) = kw_byte else { continue };
let kw_line = line_of(&line_starts, ByteOffset::new(kw_byte));
let ls = line_starts[kw_line];
if src[ls..kw_byte].chars().any(|c| c != ' ') {
continue;
}
if leading_has_tab(src, ls) {
continue;
}
let cur = indent_of(src, &line_starts, kw_line);
let delta = target - cur;
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(ls),
block_end: ByteOffset::new(branch_end),
delta: IndentDelta::new(delta),
});
}
}
}
edits
}
fn reindent_ifs(src: &str, module: &Module) -> String {
let edits = if_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn case_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let mut cases: Vec<(Span, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Case { span, alts, .. } = e {
if let (Some(first), Some(last)) = (alts.first(), alts.last()) {
cases.push((*span, first.span.start_usize(), last.span.end_usize()));
}
}
});
cases.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (case_span, first_alt, last_alt_end) in cases {
if accepted.iter().any(|a| {
a.start_usize() <= case_span.start_usize()
&& case_span.end_usize() <= a.end_usize()
&& *a != case_span
}) {
continue;
}
accepted.push(case_span);
let case_line = line_of(&line_starts, ByteOffset::new(case_span.start_usize()));
let alt_line = line_of(&line_starts, ByteOffset::new(first_alt));
if alt_line <= case_line {
continue;
}
if leading_has_tab(src, line_starts[case_line])
|| leading_has_tab(src, line_starts[alt_line])
{
continue;
}
let case_indent = indent_of(src, &line_starts, case_line);
let cur = indent_of(src, &line_starts, alt_line);
let delta = (case_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(line_starts[alt_line]),
block_end: ByteOffset::new(last_alt_end),
delta: IndentDelta::new(delta),
});
}
}
edits
}
fn reindent_cases(src: &str, module: &Module) -> String {
let edits = case_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn letin_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let mut lets: Vec<(Span, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::LetIn { span, bindings, .. } = e {
if let (Some(first), Some(last)) = (bindings.first(), bindings.last()) {
lets.push((*span, first.span.start_usize(), last.span.end_usize()));
}
}
});
lets.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (let_span, first_bind, last_bind_end) in lets {
if accepted.iter().any(|a| {
a.start_usize() <= let_span.start_usize()
&& let_span.end_usize() <= a.end_usize()
&& *a != let_span
}) {
continue;
}
accepted.push(let_span);
let let_line = line_of(&line_starts, ByteOffset::new(let_span.start_usize()));
let bind_line = line_of(&line_starts, ByteOffset::new(first_bind));
if bind_line <= let_line {
continue;
}
if src[line_starts[let_line]..let_span.start_usize()]
.chars()
.any(|c| c != ' ')
{
continue;
}
if leading_has_tab(src, line_starts[let_line])
|| leading_has_tab(src, line_starts[bind_line])
{
continue;
}
let let_indent = indent_of(src, &line_starts, let_line);
let cur = indent_of(src, &line_starts, bind_line);
let delta = (let_indent + INDENT) - cur;
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(line_starts[bind_line]),
block_end: ByteOffset::new(last_bind_end),
delta: IndentDelta::new(delta),
});
}
}
edits
}
fn reindent_letins(src: &str, module: &Module) -> String {
let edits = letin_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn con_with_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut recs: Vec<(Span, usize, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Record {
span, base, fields, ..
} = e
{
if !matches!(base.as_ref(), Expr::Con { .. }) {
return;
}
if let (Some(first), Some(last)) = (fields.first(), fields.last()) {
recs.push((
*span,
base.span().end_usize(),
first.span().start_usize(),
last.span().end_usize(),
));
}
}
});
recs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits: Vec<Edit> = Vec::new();
let mut accepted: Vec<Span> = Vec::new();
for (rec_span, base_end, first_field, last_field_end) in recs {
if accepted.iter().any(|a| {
a.start_usize() <= rec_span.start_usize()
&& rec_span.end_usize() <= a.end_usize()
&& *a != rec_span
}) {
continue;
}
accepted.push(rec_span);
let rec_line = line_of(&line_starts, ByteOffset::new(rec_span.start_usize()));
let field_line = line_of(&line_starts, ByteOffset::new(first_field));
if field_line <= rec_line {
continue;
}
match find_keyword(src, base_end, first_field, "with", &comments) {
Some(w) if line_of(&line_starts, ByteOffset::new(w)) == rec_line => {}
_ => continue,
}
if leading_has_tab(src, line_starts[rec_line])
|| leading_has_tab(src, line_starts[field_line])
{
continue;
}
let rec_indent = indent_of(src, &line_starts, rec_line);
let cur = indent_of(src, &line_starts, field_line);
let target = rec_indent + INDENT;
if next_code_line_starts_with_keyword(
src,
&line_starts,
&comments,
line_of(
&line_starts,
ByteOffset::new(last_field_end.saturating_sub(1)),
),
"where",
)
.is_some_and(|next_line| indent_of(src, &line_starts, next_line) <= target)
{
continue;
}
let delta = target - cur;
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(line_starts[field_line]),
block_end: ByteOffset::new(last_field_end),
delta: IndentDelta::new(delta),
});
}
}
edits
}
fn reindent_con_with(src: &str, module: &Module) -> String {
let edits = con_with_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_line_edit(edits: &mut Vec<Edit>, ls: &LineStarts, src: &str, line: LineIndex, target: i64) {
if leading_has_tab(src, ls[line]) {
return;
}
let delta = target - indent_of(src, ls, line);
if delta != 0 {
let end = *ls.get(line + 1).unwrap_or(&src.len());
edits.push(Edit {
child_start: ByteOffset::new(ls[line]),
block_end: ByteOffset::new(end),
delta: IndentDelta::new(delta),
});
}
}
fn push_block_edit(
edits: &mut Vec<Edit>,
ls: &LineStarts,
src: &str,
first_byte: usize,
end_byte: usize,
target: i64,
head_line: LineIndex,
) {
let first_line = line_of(ls, ByteOffset::new(first_byte));
if first_line <= head_line {
return;
}
if src[ls[first_line]..first_byte].chars().any(|c| c != ' ') {
return;
}
if leading_has_tab(src, ls[first_line]) {
return;
}
let delta = target - indent_of(src, ls, first_line);
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(ls[first_line]),
block_end: ByteOffset::new(end_byte),
delta: IndentDelta::new(delta),
});
}
}
const fn body_decl_span(d: &TemplateBodyDecl) -> Span {
match d {
TemplateBodyDecl::Signatory { span, .. }
| TemplateBodyDecl::Observer { span, .. }
| TemplateBodyDecl::Ensure { span, .. }
| TemplateBodyDecl::Key { span, .. }
| TemplateBodyDecl::Maintainer { span, .. }
| TemplateBodyDecl::Other { span, .. } => *span,
TemplateBodyDecl::Choice(c) => c.span,
TemplateBodyDecl::InterfaceInstance(i) => i.span,
_ => Span::from_usize(0, 0),
}
}
#[allow(clippy::too_many_arguments)]
fn reindent_keyword_block(
edits: &mut Vec<Edit>,
ls: &LineStarts,
src: &str,
comments: &[(usize, usize)],
head_line: LineIndex,
kw_target: i64,
body_target: i64,
kw: &str,
kw_from: usize,
block_first: usize,
block_last_end: usize,
) {
if let Some(w) = find_keyword(src, kw_from, block_first, kw, comments) {
if line_of(ls, ByteOffset::new(w)) > head_line {
push_line_edit(edits, ls, src, line_of(ls, ByteOffset::new(w)), kw_target);
}
}
push_block_edit(
edits,
ls,
src,
block_first,
block_last_end,
body_target,
head_line,
);
}
fn template_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for d in &module.decls {
let head_byte = match d {
Decl::Template(t) => t.span.start_usize(),
Decl::Interface(i) => i.span.start_usize(),
_ => continue,
};
let head_line = line_of(&line_starts, ByteOffset::new(head_byte));
if leading_has_tab(src, line_starts[head_line]) {
continue;
}
let head_indent = indent_of(src, &line_starts, head_line);
let kw_target = head_indent + INDENT;
match d {
Decl::Template(t) => {
let body_target = head_indent + 2 * INDENT;
if let (Some(f0), Some(fl)) = (t.fields.first(), t.fields.last()) {
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
body_target,
"with",
t.span.start_usize(),
f0.span.start_usize(),
fl.span.end_usize(),
);
}
if let (Some(b0), Some(bl)) = (t.body.first(), t.body.last()) {
let b0_start = body_decl_span(b0).start;
let bl_end = body_decl_span(bl).end;
let where_from = t
.fields
.last()
.map(|f| f.span.end_usize())
.unwrap_or_else(|| t.span.start_usize());
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
body_target,
"where",
where_from,
b0_start.get(),
bl_end.get(),
);
}
}
Decl::Interface(i) => {
let mut last_end = None;
for s in i
.methods
.iter()
.map(|m| m.span)
.chain(i.choices.iter().map(|c| c.span))
{
last_end = Some(
last_end.map_or_else(|| s.end_usize(), |e: usize| e.max(s.end_usize())),
);
}
if let Some(last_end) = last_end {
if let Some(fbl) =
first_code_line_after(src, &line_starts, &comments, head_line, last_end)
{
reindent_keyword_block(
&mut edits,
&line_starts,
src,
&comments,
head_line,
kw_target,
head_indent + INDENT,
"where",
i.span.start_usize(),
line_starts[fbl],
last_end,
);
}
}
}
_ => {}
}
}
edits
}
fn reindent_templates(src: &str, module: &Module) -> String {
let edits = template_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn module_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
if !module.header.is_empty() {
push_continuation_lines(
&mut edits,
src,
&line_starts,
&comments,
module.header,
INDENT,
);
}
for imp in &module.imports {
push_continuation_lines(&mut edits, src, &line_starts, &comments, imp.span, INDENT);
}
edits
}
fn reindent_modules_and_imports(src: &str, module: &Module) -> String {
let edits = module_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_continuation_lines(
edits: &mut Vec<Edit>,
src: &str,
ls: &LineStarts,
comments: &[(usize, usize)],
span: Span,
offset: i64,
) {
let head_line = line_of(ls, ByteOffset::new(span.start_usize()));
let target = indent_of(src, ls, head_line) + offset;
let mut line = head_line + 1;
while line.get() < ls.len() && ls[line] < span.end_usize() {
push_code_line_edit(edits, src, ls, comments, line, target);
line += 1;
}
}
fn collect_choices<'a>(module: &'a Module, choices: &mut Vec<&'a ChoiceDecl>) {
for decl in &module.decls {
match decl {
Decl::Template(t) => {
for body in &t.body {
if let TemplateBodyDecl::Choice(c) = body {
choices.push(c);
}
}
}
Decl::Interface(i) => choices.extend(i.choices.iter()),
_ => {}
}
}
choices.sort_by(|a, b| {
a.span
.start
.cmp(&b.span.start)
.then(b.span.end_usize().cmp(&a.span.end_usize()))
});
}
fn choice_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut choices = Vec::new();
collect_choices(module, &mut choices);
let mut edits = Vec::new();
for c in choices {
let choice_line = line_of(&line_starts, ByteOffset::new(c.span.start_usize()));
if leading_has_tab(src, line_starts[choice_line]) {
continue;
}
let choice_indent = indent_of(src, &line_starts, choice_line);
let clause_target = choice_indent + INDENT;
let nested_target = choice_indent + 2 * INDENT;
if let TypeAnnotation::Present(ty) = &c.return_ty {
if let Some(colon) = find_symbol(
src,
c.span.start_usize(),
ty.span().start_usize(),
":",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
colon,
ty.span().end_usize(),
clause_target,
);
}
}
if let (Some(first), Some(last)) = (c.params.first(), c.params.last()) {
let kw_from = c
.return_ty
.as_type()
.map_or_else(|| c.span.start_usize(), |t| t.span().end_usize());
if let Some(w) = find_keyword(src, kw_from, first.span.start_usize(), "with", &comments)
{
let with_line = line_of(&line_starts, ByteOffset::new(w));
let first_param_line =
line_of(&line_starts, ByteOffset::new(first.span.start_usize()));
if first_param_line == with_line {
push_span_block_edit(
&mut edits,
&line_starts,
src,
w,
last.span.end_usize(),
clause_target,
);
} else {
push_line_edit(&mut edits, &line_starts, src, with_line, clause_target);
push_block_edit(
&mut edits,
&line_starts,
src,
first.span.start_usize(),
last.span.end_usize(),
nested_target,
choice_line,
);
}
}
}
if let (Some(first), Some(last)) = (c.observers.first(), c.observers.last()) {
if let Some(k) = find_keyword(
src,
c.span.start_usize(),
first.span().start_usize(),
"observer",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
k,
last.span().end_usize(),
clause_target,
);
}
}
if let (Some(first), Some(last)) = (c.controllers.first(), c.controllers.last()) {
if let Some(k) = find_keyword(
src,
c.span.start_usize(),
first.span().start_usize(),
"controller",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
k,
last.span().end_usize(),
clause_target,
);
}
}
if let (Some(first), Some(last)) = (c.authority_exprs.first(), c.authority_exprs.last()) {
if let Some(k) = find_keyword(
src,
c.span.start_usize(),
first.span().start_usize(),
"authority",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
k,
last.span().end_usize(),
clause_target,
);
}
}
if let Some(body) = &c.body {
push_span_block_edit(
&mut edits,
&line_starts,
src,
body.span().start_usize(),
body.span().end_usize(),
clause_target,
);
}
}
edits
}
fn reindent_choices(src: &str, module: &Module) -> String {
let edits = choice_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn type_def_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for decl in &module.decls {
let Decl::TypeDef { span, .. } = decl else {
continue;
};
let head_line = line_of(&line_starts, ByteOffset::new(span.start_usize()));
if leading_has_tab(src, line_starts[head_line]) {
continue;
}
let head_indent = indent_of(src, &line_starts, head_line);
let head_has_with = line_contains_word(src, &line_starts, head_line, "with");
let mut in_with = head_has_with;
let mut with_body_target = if head_has_with {
first_body_anchor_indent_after(
src,
&line_starts,
&comments,
head_line,
span.end_usize(),
)
.unwrap_or(head_indent + INDENT)
} else {
head_indent + 2 * INDENT
};
let head_has_where = line_contains_word(src, &line_starts, head_line, "where");
let mut in_where = head_has_where;
let mut where_body_target = if head_has_where {
first_body_anchor_indent_after(
src,
&line_starts,
&comments,
head_line,
span.end_usize(),
)
.unwrap_or(head_indent + INDENT)
} else {
head_indent + INDENT
};
let mut after_variant = line_contains_symbol(src, &line_starts, head_line, "=");
let mut after_bar_variant = false;
let mut line = head_line + 1;
while line.get() < line_starts.len() && line_starts[line] < span.end_usize() {
let Some(trimmed) = code_line_trimmed(src, &line_starts, &comments, line) else {
line += 1;
continue;
};
let target = if starts_with_word(trimmed, "where") {
in_with = false;
in_where = true;
where_body_target = head_indent + 2 * INDENT;
after_variant = false;
Some(head_indent + INDENT)
} else if starts_with_word(trimmed, "with") {
let target = if after_bar_variant {
head_indent + 2 * INDENT
} else {
head_indent + INDENT
};
in_with = true;
in_where = false;
with_body_target = target + INDENT;
after_variant = false;
after_bar_variant = false;
Some(target)
} else if trimmed.starts_with('=') || trimmed.starts_with('|') {
in_with = false;
in_where = false;
after_variant = true;
after_bar_variant = trimmed.starts_with('|');
Some(head_indent + INDENT)
} else if starts_with_word(trimmed, "deriving") {
in_with = false;
in_where = false;
after_variant = false;
after_bar_variant = false;
Some(head_indent + INDENT)
} else if in_with {
Some(with_body_target)
} else if in_where {
Some(where_body_target)
} else if after_variant {
Some(head_indent + INDENT)
} else {
None
};
if let Some(target) = target {
push_code_line_edit(&mut edits, src, &line_starts, &comments, line, target);
}
line += 1;
}
}
edits
}
fn first_body_anchor_indent_after(
src: &str,
ls: &LineStarts,
comments: &[(usize, usize)],
after_line: LineIndex,
block_end: usize,
) -> Option<i64> {
let mut line = after_line + 1;
while line.get() < ls.len() && ls[line] < block_end {
if leading_has_tab(src, ls[line]) {
return None;
}
let end = *ls.get(line + 1).unwrap_or(&src.len());
let text = &src[ls[line]..end];
let cur = text.len() - text.trim_start_matches(' ').len();
let trimmed = text[cur..].trim_end();
if trimmed.is_empty() {
line += 1;
continue;
}
if is_comment_line(comments, ls[line] + cur) && !trimmed.starts_with("{-#") {
line += 1;
continue;
}
return Some(indent_of(src, ls, line));
}
None
}
fn reindent_type_defs(src: &str, module: &Module) -> String {
let edits = type_def_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn guard_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
for decl in &module.decls {
let Decl::Function(fun) = decl else {
continue;
};
for eq in &fun.equations {
let eq_line = line_of(&line_starts, ByteOffset::new(eq.span.start_usize()));
if leading_has_tab(src, line_starts[eq_line]) {
continue;
}
let guard_target = indent_of(src, &line_starts, eq_line) + INDENT;
let mut cursor = eq.span.start_usize();
for (guard, body) in &eq.guards {
if let Some(pipe) =
find_symbol(src, cursor, guard.span().start_usize(), "|", &comments)
{
push_span_block_edit(
&mut edits,
&line_starts,
src,
pipe,
body.span().end_usize(),
guard_target,
);
}
cursor = body.span().end_usize();
}
if let (Some(first), Some(last)) = (eq.where_bindings.first(), eq.where_bindings.last())
{
if let Some(w) =
find_keyword(src, cursor, first.span.start_usize(), "where", &comments)
{
push_span_block_edit(
&mut edits,
&line_starts,
src,
w,
w + "where".len(),
guard_target,
);
push_block_edit(
&mut edits,
&line_starts,
src,
first.span.start_usize(),
last.span.end_usize(),
guard_target + INDENT,
eq_line,
);
}
}
}
}
edits
}
fn reindent_guards(src: &str, module: &Module) -> String {
let edits = guard_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn record_update_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut recs: Vec<(Span, usize, usize, usize)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Record {
span, base, fields, ..
} = e
{
if matches!(base.as_ref(), Expr::Con { .. }) {
return;
}
if let (Some(first), Some(last)) = (fields.first(), fields.last()) {
recs.push((
*span,
base.span().end_usize(),
first.span().start_usize(),
last.span().end_usize(),
));
}
}
});
recs.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits = Vec::new();
for (rec_span, base_end, first_field, last_field_end) in recs {
let rec_line = line_of(&line_starts, ByteOffset::new(rec_span.start_usize()));
let field_line = line_of(&line_starts, ByteOffset::new(first_field));
if field_line <= rec_line || leading_has_tab(src, line_starts[rec_line]) {
continue;
}
let Some(w) = find_keyword(src, base_end, first_field, "with", &comments) else {
continue;
};
let with_line = line_of(&line_starts, ByteOffset::new(w));
let rec_indent = indent_of(src, &line_starts, rec_line);
let field_target = if with_line > rec_line {
push_line_edit(
&mut edits,
&line_starts,
src,
with_line,
rec_indent + INDENT,
);
rec_indent + 2 * INDENT
} else {
rec_indent + INDENT
};
push_block_edit(
&mut edits,
&line_starts,
src,
first_field,
last_field_end,
field_target,
rec_line,
);
}
edits
}
fn reindent_record_updates(src: &str, module: &Module) -> String {
let edits = record_update_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn try_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut tries: Vec<(Span, Span, Vec<Span>)> = Vec::new();
walk_module_expressions(module, &mut |e| {
if let Expr::Try {
span,
body,
handlers,
..
} = e
{
tries.push((
*span,
body.span(),
handlers.iter().map(|h| h.span).collect(),
));
}
});
tries.sort_by(|a, b| a.0.start.cmp(&b.0.start).then(b.0.end.cmp(&a.0.end)));
let mut edits = Vec::new();
for (try_span, body_span, handlers) in tries {
let try_line = line_of(&line_starts, ByteOffset::new(try_span.start_usize()));
if leading_has_tab(src, line_starts[try_line]) {
continue;
}
let try_col = usize_to_i64_saturating(
src[line_starts[try_line]..try_span.start_usize()]
.chars()
.count(),
);
let nested_target = try_col + INDENT;
push_block_edit(
&mut edits,
&line_starts,
src,
body_span.start_usize(),
body_span.end_usize(),
nested_target,
try_line,
);
if let Some(first_handler) = handlers.first() {
if let Some(catch) = find_keyword(
src,
body_span.end_usize(),
first_handler.start_usize(),
"catch",
&comments,
) {
push_span_block_edit(
&mut edits,
&line_starts,
src,
catch,
catch + "catch".len(),
try_col,
);
}
}
if let (Some(first), Some(last)) = (handlers.first(), handlers.last()) {
push_block_edit(
&mut edits,
&line_starts,
src,
first.start_usize(),
last.end_usize(),
nested_target,
try_line,
);
}
}
edits
}
fn reindent_tries(src: &str, module: &Module) -> String {
let edits = try_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn continuation_edits(src: &str, module: &Module) -> Vec<Edit> {
let line_starts = line_start_table(src);
let comments = comment_spans(src);
let mut edits = Vec::new();
walk_module_expressions(module, &mut |e| match e {
Expr::Tuple { span, items, .. } | Expr::List { span, items, .. } => {
push_item_continuations(
&mut edits,
src,
&line_starts,
&comments,
*span,
items.iter().map(Expr::span),
);
}
_ => {}
});
edits
}
fn push_item_continuations(
edits: &mut Vec<Edit>,
src: &str,
ls: &LineStarts,
comments: &[(usize, usize)],
span: Span,
items: impl Iterator<Item = Span>,
) {
let head_line = line_of(ls, ByteOffset::new(span.start_usize()));
let target = indent_of(src, ls, head_line) + INDENT;
for item in items {
let item_line = line_of(ls, ByteOffset::new(item.start_usize()));
if item_line <= head_line {
continue;
}
let mut first = item.start_usize();
let line_start = ls[item_line];
if let Some(comma) = src[line_start..item.start_usize()].rfind(',') {
first = line_start + comma;
}
if is_comment_line(comments, first) {
continue;
}
push_span_block_edit(edits, ls, src, first, item.end_usize(), target);
}
}
fn reindent_continuations(src: &str, module: &Module) -> String {
let edits = continuation_edits(src, module);
if edits.is_empty() {
return src.to_string();
}
apply_shifts(src, &edits)
}
fn push_span_block_edit(
edits: &mut Vec<Edit>,
ls: &LineStarts,
src: &str,
first_byte: usize,
end_byte: usize,
target: i64,
) {
let first_line = line_of(ls, ByteOffset::new(first_byte));
if src[ls[first_line]..first_byte].chars().any(|c| c != ' ') {
return;
}
if leading_has_tab(src, ls[first_line]) {
return;
}
let delta = target - indent_of(src, ls, first_line);
if delta != 0 {
edits.push(Edit {
child_start: ByteOffset::new(ls[first_line]),
block_end: ByteOffset::new(end_byte),
delta: IndentDelta::new(delta),
});
}
}
fn push_code_line_edit(
edits: &mut Vec<Edit>,
src: &str,
ls: &LineStarts,
comments: &[(usize, usize)],
line: LineIndex,
target: i64,
) {
if code_line_trimmed(src, ls, comments, line).is_none() || leading_has_tab(src, ls[line]) {
return;
}
push_line_edit(edits, ls, src, line, target);
}
fn code_line_trimmed<'a>(
src: &'a str,
ls: &LineStarts,
comments: &[(usize, usize)],
line: LineIndex,
) -> Option<&'a str> {
let start = *ls.get(line)?;
let end = *ls.get(line + 1).unwrap_or(&src.len());
let text = &src[start..end];
let cur = text.len() - text.trim_start_matches(' ').len();
let trimmed = text[cur..].trim_end();
if trimmed.is_empty() || is_comment_line(comments, start + cur) {
None
} else {
Some(trimmed)
}
}
fn starts_with_word(s: &str, word: &str) -> bool {
s.strip_prefix(word).is_some_and(|rest| {
rest.is_empty()
|| rest
.chars()
.next()
.is_some_and(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '\'')
})
}
fn line_contains_word(src: &str, ls: &LineStarts, line: LineIndex, word: &str) -> bool {
let end = *ls.get(line + 1).unwrap_or(&src.len());
let line_text = &src[ls[line]..end];
let mut i = 0;
while let Some(rel) = line_text[i..].find(word) {
let at = i + rel;
let before_ok = at == 0 || !is_ident_byte(line_text.as_bytes()[at - 1]);
let after = at + word.len();
let after_ok = after >= line_text.len() || !is_ident_byte(line_text.as_bytes()[after]);
if before_ok && after_ok {
return true;
}
i = at + 1;
}
false
}
const fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'\''
}
fn line_contains_symbol(src: &str, ls: &LineStarts, line: LineIndex, symbol: &str) -> bool {
let end = *ls.get(line + 1).unwrap_or(&src.len());
src[ls[line]..end].contains(symbol)
}
fn find_symbol(
src: &str,
from: usize,
to: usize,
symbol: &str,
comments: &[(usize, usize)],
) -> Option<usize> {
let hay = &src[from..to.min(src.len())];
let mut i = 0;
while let Some(rel) = hay[i..].find(symbol) {
let abs = from + i + rel;
if !is_comment_line(comments, abs) {
return Some(abs);
}
i += rel + symbol.len();
}
None
}
#[cfg(test)]
mod line_helpers {
use super::*;
#[test]
fn comment_line_detection() {
let src = "x\n-- hi\ny\n";
let comments = comment_spans(src);
assert!(is_comment_line(&comments, 2));
assert!(!is_comment_line(&comments, 0)); assert!(!is_comment_line(&comments, 7)); }
#[test]
fn indent_and_line_helpers() {
let src = "a\n b\n";
let ls = line_start_table(src);
assert_eq!(indent_of(src, &ls, LineIndex::new(0)), 0);
assert_eq!(indent_of(src, &ls, LineIndex::new(1)), 4);
assert_eq!(line_of(&ls, ByteOffset::new(0)), LineIndex::new(0));
assert_eq!(line_of(&ls, ByteOffset::new(6)), LineIndex::new(1));
}
#[test]
fn formatter_coordinate_domains_are_distinct_types() {
use std::any::TypeId;
assert_ne!(TypeId::of::<ByteOffset>(), TypeId::of::<LineIndex>());
assert_ne!(TypeId::of::<ByteOffset>(), TypeId::of::<IndentDelta>());
assert_ne!(TypeId::of::<LineIndex>(), TypeId::of::<IndentDelta>());
}
#[test]
fn import_groups_are_named_domains_with_expected_order() {
assert_eq!(import_group("Daml.Script"), ImportGroup::DamlStdlib);
assert_eq!(import_group("DA.List"), ImportGroup::DaLibrary);
assert_eq!(import_group("My.App"), ImportGroup::LocalOrExternal);
assert!(ImportGroup::DamlStdlib < ImportGroup::DaLibrary);
assert!(ImportGroup::DaLibrary < ImportGroup::LocalOrExternal);
}
}