use super::*;
impl<'source, 'facts, S> ShellRenderer<'source, 'facts, S>
where
S: StreamSink,
{
pub(super) fn write_comment(&mut self, comment: &SourceComment<'_>) {
self.write_text(comment.text());
}
pub(super) fn emit_leading_comments(
&mut self,
comments: &[SourceComment<'_>],
next_line: usize,
) {
for (index, comment) in comments.iter().enumerate() {
self.write_comment(comment);
let target_line = comments
.get(index + 1)
.map(SourceComment::line)
.unwrap_or(next_line);
self.write_line_breaks(line_gap_break_count(comment.line(), target_line));
}
}
pub(super) fn emit_pipeline_leading_comments_after_operator(
&mut self,
comments: &[SourceComment<'_>],
next_line: usize,
operator_end: usize,
) {
let preserve_blank_before_comments = comments.first().is_some_and(|comment| {
gap_has_blank_line(self.source(), operator_end, comment.span().start.offset())
});
if preserve_blank_before_comments {
self.newline();
}
for (index, comment) in comments.iter().enumerate() {
self.write_comment(comment);
let target_line = comments
.get(index + 1)
.map(SourceComment::line)
.unwrap_or(next_line);
let breaks = if preserve_blank_before_comments && index + 1 == comments.len() {
1
} else {
line_gap_break_count(comment.line(), target_line)
};
self.write_line_breaks(breaks);
}
}
pub(super) fn emit_trailing_comments_for_stmt(&mut self, comments: &[SourceComment<'source>]) {
for comment in comments {
self.write_trailing_comment(comment, false);
}
}
pub(super) fn emit_dangling_comments(&mut self, comments: &[SourceComment<'_>]) {
self.emit_dangling_comments_after(comments, None);
}
pub(super) fn emit_dangling_comments_after(
&mut self,
comments: &[SourceComment<'_>],
previous_line: Option<usize>,
) {
for (index, comment) in comments.iter().enumerate() {
if index == 0 {
if let Some(previous_line) = previous_line {
self.write_line_breaks(line_gap_break_count(previous_line, comment.line()));
} else {
self.newline();
}
}
self.maybe_preserve_dangling_comment_outdent(comment);
self.write_comment(comment);
if let Some(next) = comments.get(index + 1) {
self.write_line_breaks(line_gap_break_count(comment.line(), next.line()));
}
}
}
pub(super) fn stmt_rendered_end_line(&self, stmt: &Stmt) -> usize {
stmt_rendered_end_line_after_format(
stmt,
self.source(),
self.source_map(),
self.facts(),
self.facts().stmt(stmt).rendered_end_line(),
)
}
pub(super) fn stmt_sequence_next_start_line(
&self,
statements: &StmtSeq,
index: usize,
attachments: Option<SequenceRenderSnapshot<'source, 'facts>>,
) -> usize {
attachments
.map(|attachment| attachment.first_rendered_line_for(index + 1))
.unwrap_or_else(|| {
self.facts()
.stmt(&statements[index + 1])
.rendered_start_line()
})
}
pub(super) fn maybe_preserve_dangling_comment_outdent(&mut self, comment: &SourceComment<'_>) {
if !self.line_start() {
return;
}
if comment_precedes_close_keyword_at_same_indent(self.source(), self.source_map(), comment)
{
let close_indent_column =
self.indent_column_for_level(self.indent_level().saturating_sub(1));
if close_indent_column == 0 {
self.writer.mark_line_started_with_zero_indent();
} else {
self.write_indent_to_column(close_indent_column);
}
}
}
pub(super) fn write_close_suffix_after_span(&mut self, close_span: Option<Span>) {
let Some(plan) =
close_span.and_then(|span| self.facts().close_suffix_comment_plan_after_span(span))
else {
return;
};
self.write_inline_comment_plan(plan, false);
}
pub(super) fn write_inline_comment_plan(
&mut self,
plan: crate::facts::InlineCommentPlan<'source>,
nudge_aligned: bool,
) {
let current_code_column = self.column().saturating_sub(self.line_indent_column());
let mut padding = plan.padding(
self.source_map(),
current_code_column,
self.line_indent_column(),
);
if nudge_aligned && plan.has_alignment(self.source_map(), self.line_indent_column()) {
padding += 1;
}
self.write_spaces(padding);
self.write_comment(&plan.comment());
}
pub(super) fn write_trailing_comment(
&mut self,
comment: &SourceComment<'source>,
nudge_aligned: bool,
) {
let plan = self.facts().trailing_comment_plan(*comment);
self.write_inline_comment_plan(plan, nudge_aligned);
}
pub(super) fn write_suffix_comment_after_span(&mut self, span: Span, nudge_aligned: bool) {
let Some(plan) = self.facts().suffix_comment_plan_for_span(span) else {
self.write_space();
self.write_text(span.slice(self.source()).trim_start());
return;
};
self.write_inline_comment_plan(plan, nudge_aligned);
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct CommentAlignmentFacts {
source_indent_column: usize,
trailing_target_column: Option<usize>,
trailing_indent_adjust: usize,
close_suffix_target_column: Option<usize>,
}
impl CommentAlignmentFacts {
pub(crate) fn new(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
) -> Self {
let source_indent_column = source_map
.line_indent_before_offset(comment.span().start.offset())
.map_or(0, |indent| indent.chars().count());
Self {
source_indent_column,
trailing_target_column: trailing_comment_alignment_column(
source,
source_map,
comment,
source_indent_column,
),
trailing_indent_adjust: trailing_comment_tab_indent_adjust(source, source_map, comment),
close_suffix_target_column: aligned_close_suffix_comment_target_column(
source,
source_map,
comment,
source_indent_column,
),
}
}
pub(crate) fn trailing_padding(
&self,
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_code_column: usize,
current_indent_column: usize,
) -> usize {
let Some(target_column) =
self.trailing_target_column(source, source_map, comment, current_indent_column)
else {
return 1;
};
target_column
.saturating_sub(current_code_column.saturating_add(self.trailing_indent_adjust))
.max(1)
}
pub(crate) fn has_trailing_alignment(
&self,
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_indent_column: usize,
) -> bool {
self.trailing_target_column(source, source_map, comment, current_indent_column)
.is_some()
}
pub(crate) fn close_suffix_padding(
&self,
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_code_column: usize,
current_indent_column: usize,
) -> usize {
if let Some(target_column) =
self.close_suffix_target_column(source, source_map, comment, current_indent_column)
{
return target_column
.saturating_sub(current_indent_column + current_code_column)
.max(1);
}
self.trailing_padding(
source,
source_map,
comment,
current_code_column,
current_indent_column,
)
}
fn trailing_target_column(
&self,
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_indent_column: usize,
) -> Option<usize> {
if current_indent_column == self.source_indent_column {
return self.trailing_target_column;
}
trailing_comment_alignment_column(source, source_map, comment, current_indent_column)
}
fn close_suffix_target_column(
&self,
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_indent_column: usize,
) -> Option<usize> {
if current_indent_column == self.source_indent_column {
return self.close_suffix_target_column;
}
aligned_close_suffix_comment_target_column(
source,
source_map,
comment,
current_indent_column,
)
}
}
fn aligned_close_suffix_comment_target_column(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_indent_column: usize,
) -> Option<usize> {
let entries = close_suffix_comment_alignment_entries(source, source_map, comment)?;
if entries.len() <= 1 {
return None;
}
let current = entries.first()?;
let source_indent_unit = if current.source_indent > 0 && current_indent_column > 0 {
current.source_indent / current_indent_column
} else {
entries
.iter()
.filter_map(|entry| (entry.source_indent > 0).then_some(entry.source_indent))
.min()
.unwrap_or(1)
}
.max(1);
Some(
entries
.iter()
.map(|entry| {
rendered_close_suffix_source_indent(
entry.source_indent,
current.source_indent,
current_indent_column,
source_indent_unit,
) + entry.code_width
})
.max()?
+ 1,
)
}
#[derive(Clone, Copy)]
struct CloseSuffixAlignmentEntry {
source_indent: usize,
code_width: usize,
}
fn close_suffix_comment_alignment_entries(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
) -> Option<Vec<CloseSuffixAlignmentEntry>> {
let (line_start, line_end) =
source_map.line_bounds_for_offset(comment.span().start.offset())?;
let mut entries = vec![close_suffix_alignment_entry(
source,
line_start,
line_end,
Some(comment.span().start.offset()),
)?];
let mut previous_start = line_start;
while let Some((start, end)) = source_map.previous_line_bounds(previous_start) {
let Some(entry) = close_suffix_alignment_entry(source, start, end, None) else {
break;
};
entries.push(entry);
previous_start = start;
}
let mut next_line = source_map.next_line_bounds(line_end);
while let Some((start, end)) = next_line {
let Some(entry) = close_suffix_alignment_entry(source, start, end, None) else {
break;
};
entries.push(entry);
next_line = source_map.next_line_bounds(end);
}
Some(entries)
}
fn close_suffix_alignment_entry(
source: &str,
line_start: usize,
line_end: usize,
known_comment_offset: Option<usize>,
) -> Option<CloseSuffixAlignmentEntry> {
let comment_offset = known_comment_offset
.or_else(|| find_inline_comment_start(source.get(line_start..line_end)?, line_start))?;
let prefix = source.get(line_start..comment_offset)?;
let code = prefix.trim_matches([' ', '\t', '\r']);
if !matches!(code, "fi" | "done" | "esac" | "}") {
return None;
}
let (source_indent, _) = leading_indent_and_code_start(prefix)?;
Some(CloseSuffixAlignmentEntry {
source_indent,
code_width: normalized_comment_alignment_width(code),
})
}
fn rendered_close_suffix_source_indent(
source_indent: usize,
current_source_indent: usize,
current_rendered_indent: usize,
source_indent_unit: usize,
) -> usize {
if source_indent == current_source_indent {
return current_rendered_indent;
}
if current_source_indent == 0 || current_rendered_indent == 0 {
return source_indent / source_indent_unit;
}
source_indent.saturating_mul(current_rendered_indent) / current_source_indent
}
fn leading_indent_and_code_start(text: &str) -> Option<(usize, usize)> {
let mut indent_width = 0;
for (index, ch) in text.char_indices() {
match ch {
' ' | '\t' => indent_width += 1,
'\r' => {}
_ => return Some((indent_width, index)),
}
}
None
}
pub(super) fn trailing_comment_alignment_column(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
current_indent_column: usize,
) -> Option<usize> {
let (line_start, line_end) =
source_map.line_bounds_for_offset(comment.span().start.offset())?;
let mut widths = vec![trimmed_line_width(
source.get(line_start..comment.span().start.offset())?,
)?];
let mut previous_start = line_start;
while let Some((start, end)) = source_map.previous_line_bounds(previous_start) {
let Some(width) = inline_comment_code_width(source, start, end, None) else {
if source
.get(start..end)
.is_some_and(line_is_skippable_alignment_opener)
{
previous_start = start;
continue;
}
break;
};
widths.push(width);
previous_start = start;
}
let mut next_line = source_map.next_line_bounds(line_end);
while let Some((start, end)) = next_line {
let Some(width) = inline_comment_code_width(source, start, end, None) else {
if let Some((width, suffix_end)) = multiline_header_suffix_comment_width(
source,
source_map,
line_start,
start,
end,
current_indent_column,
) {
widths.push(width);
next_line = source_map.next_line_bounds(suffix_end);
continue;
}
break;
};
widths.push(width);
next_line = source_map.next_line_bounds(end);
}
(widths.len() > 1).then(|| widths.into_iter().max().unwrap_or(0) + 1)
}
fn trailing_comment_tab_indent_adjust(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
) -> usize {
let Some((line_start, line_end)) =
source_map.line_bounds_for_offset(comment.span().start.offset())
else {
return 0;
};
let Some(current_line) = source.get(line_start..comment.span().start.offset()) else {
return 0;
};
let current_tabs = leading_tabs_only_indent_width(current_line);
if current_tabs == 0 {
return 0;
}
let mut previous_start = line_start;
while let Some((start, end)) = source_map.previous_line_bounds(previous_start) {
if inline_comment_code_width(source, start, end, None).is_some() {
return source
.get(start..end)
.map(leading_tabs_only_indent_width)
.map_or(0, |previous_tabs| {
if previous_tabs == 0 {
0
} else {
current_tabs.saturating_sub(previous_tabs)
}
});
}
if source
.get(start..end)
.is_some_and(line_is_skippable_alignment_opener)
{
previous_start = start;
continue;
}
break;
}
let mut next_line = source_map.next_line_bounds(line_end);
while let Some((start, end)) = next_line {
if inline_comment_code_width(source, start, end, None).is_some() {
return source
.get(start..end)
.map(leading_tabs_only_indent_width)
.map_or(0, |next_tabs| {
if next_tabs == 0 {
0
} else {
current_tabs.saturating_sub(next_tabs)
}
});
}
if source
.get(start..end)
.is_some_and(line_is_skippable_alignment_opener)
{
next_line = source_map.next_line_bounds(end);
continue;
}
break;
}
0
}
fn leading_tabs_only_indent_width(line: &str) -> usize {
let mut tabs = 0;
for ch in line.chars() {
match ch {
'\t' => tabs += 1,
' ' | '\r' => return 0,
_ => break,
}
}
tabs
}
pub(super) fn inline_comment_code_width(
source: &str,
line_start: usize,
line_end: usize,
known_comment_offset: Option<usize>,
) -> Option<usize> {
let comment_offset = known_comment_offset
.or_else(|| find_inline_comment_start(source.get(line_start..line_end)?, line_start))?;
let prefix = source.get(line_start..comment_offset)?;
if line_is_skippable_alignment_opener(prefix) {
return None;
}
trimmed_line_width(prefix)
}
fn multiline_header_suffix_comment_width(
source: &str,
source_map: &SourceMap<'_>,
current_line_start: usize,
header_start: usize,
header_end: usize,
current_indent_column: usize,
) -> Option<(usize, usize)> {
let header_line = source.get(header_start..header_end)?;
if find_inline_comment_start(header_line, header_start).is_some() {
return None;
}
let header = header_line.trim_matches([' ', '\t', '\r']);
let suffix = multiline_header_suffix_keyword(header)?;
let (suffix_start, suffix_end) = source_map.next_line_bounds(header_end)?;
let suffix_line = source.get(suffix_start..suffix_end)?;
let comment_offset = find_inline_comment_start(suffix_line, suffix_start)?;
let suffix_prefix = source.get(suffix_start..comment_offset)?;
if suffix_prefix.trim_matches([' ', '\t', '\r']) != suffix {
return None;
}
let header = header.trim_end_matches(';').trim_end();
let rendered = format!("{header}; {suffix}");
Some((
alignment_width_relative_to_current_indent(
source,
source_map,
current_line_start,
header_start,
normalized_comment_alignment_width(&rendered),
current_indent_column,
),
suffix_end,
))
}
fn multiline_header_suffix_keyword(header: &str) -> Option<&'static str> {
match header.split_whitespace().next()? {
"if" | "elif" => Some("then"),
"for" | "select" | "until" | "while" => Some("do"),
_ => None,
}
}
fn alignment_width_relative_to_current_indent(
source: &str,
source_map: &SourceMap<'_>,
current_line_start: usize,
target_line_start: usize,
width: usize,
current_indent_column: usize,
) -> usize {
let Some((_, current_line_end)) = source_map.line_bounds_for_offset(current_line_start) else {
return width;
};
let Some((_, target_line_end)) = source_map.line_bounds_for_offset(target_line_start) else {
return width;
};
let current_indent = line_indent_info(source, current_line_start, current_line_end);
let target_indent = line_indent_info(source, target_line_start, target_line_end);
if current_indent.has_tabs || target_indent.has_tabs {
return width;
}
if let Some(adjusted) = list_rhs_to_branch_header_alignment_width(
source,
current_line_start,
current_line_end,
target_line_start,
target_line_end,
width,
) {
return adjusted;
}
let delta = rendered_indent_delta_between_lines(
source,
source_map,
current_line_start,
target_line_start,
current_indent_column,
);
if delta >= 0 {
width.saturating_add(delta as usize)
} else {
width.saturating_sub(delta.unsigned_abs())
}
}
fn rendered_indent_delta_between_lines(
source: &str,
source_map: &SourceMap<'_>,
current_line_start: usize,
target_line_start: usize,
current_rendered_indent: usize,
) -> isize {
let Some((_, current_line_end)) = source_map.line_bounds_for_offset(current_line_start) else {
return 0;
};
let Some((_, target_line_end)) = source_map.line_bounds_for_offset(target_line_start) else {
return 0;
};
let current_indent = line_indent_info(source, current_line_start, current_line_end);
let target_indent = line_indent_info(source, target_line_start, target_line_end);
if target_indent == current_indent {
return 0;
}
let target_rendered_indent = rendered_source_indent_relative_to_current(
target_indent.width,
current_indent.width,
current_rendered_indent,
);
target_rendered_indent as isize - current_rendered_indent as isize
}
fn rendered_source_indent_relative_to_current(
target_source_indent: usize,
current_source_indent: usize,
current_rendered_indent: usize,
) -> usize {
if target_source_indent == current_source_indent {
return current_rendered_indent;
}
if current_source_indent == 0 || current_rendered_indent == 0 {
let delta = target_source_indent.abs_diff(current_source_indent);
let unit = if current_source_indent == 0 {
delta
} else {
current_source_indent.min(delta)
}
.max(1);
return target_source_indent / unit;
}
let scaled = target_source_indent.saturating_mul(current_rendered_indent);
if target_source_indent < current_source_indent {
scaled / current_source_indent
} else {
scaled.div_ceil(current_source_indent)
}
}
fn list_rhs_to_branch_header_alignment_width(
source: &str,
current_line_start: usize,
current_line_end: usize,
target_line_start: usize,
target_line_end: usize,
width: usize,
) -> Option<usize> {
let current_code = source
.get(current_line_start..current_line_end)?
.trim_start_matches([' ', '\t', '\r']);
if !current_code.starts_with("||") && !current_code.starts_with("&&") {
return None;
}
let target_code = source
.get(target_line_start..target_line_end)?
.trim_start_matches([' ', '\t', '\r']);
target_code
.starts_with("elif ")
.then(|| width.saturating_sub(2))
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct LineIndentInfo {
width: usize,
has_tabs: bool,
}
fn line_indent_info(source: &str, line_start: usize, line_end: usize) -> LineIndentInfo {
source
.get(line_start..line_end)
.map(|line| {
let indent = line_leading_indent(line);
LineIndentInfo {
width: indent.chars().count(),
has_tabs: indent.contains('\t'),
}
})
.unwrap_or(LineIndentInfo {
width: 0,
has_tabs: false,
})
}
fn line_leading_indent(line: &str) -> &str {
let end = line
.char_indices()
.find(|(_, ch)| !matches!(ch, ' ' | '\t' | '\r'))
.map_or(line.len(), |(index, _)| index);
&line[..end]
}
fn find_inline_comment_start(line: &str, line_start: usize) -> Option<usize> {
for (index, ch) in line.char_indices() {
if ch != '#' {
continue;
}
let prefix = &line[..index];
if prefix.trim().is_empty() {
return None;
}
if prefix
.chars()
.last()
.is_some_and(|ch| ch == ' ' || ch == '\t')
{
return Some(line_start + index);
}
}
None
}
fn trimmed_line_width(text: &str) -> Option<usize> {
let trimmed = text
.trim_start_matches([' ', '\t', '\r'])
.trim_end_matches([' ', '\t', '\r']);
(!trimmed.trim().is_empty()).then(|| normalized_comment_alignment_width(trimmed))
}
fn normalized_comment_alignment_width(text: &str) -> usize {
let collapsed = collapse_horizontal_whitespace_runs(text);
let semicolon_normalized = trim_trailing_semicolon_for_alignment(&collapsed);
let redirect_normalized = trim_redirect_padding_for_alignment(&semicolon_normalized);
let array_normalized = trim_compound_assignment_padding_for_alignment(&redirect_normalized);
let parameter_normalized =
normalize_empty_parameter_replacements_for_alignment(&array_normalized);
let normalized = trim_arithmetic_expansion_padding_for_alignment(¶meter_normalized);
normalized.chars().count()
+ case_pattern_pipe_alignment_width(&normalized)
+ moved_function_brace_alignment_width(&normalized)
}
fn trim_trailing_semicolon_for_alignment(text: &str) -> String {
let trimmed = text.trim_end_matches([' ', '\t', '\r']);
let Some(without_semicolon) = trimmed.strip_suffix(';') else {
return text.to_string();
};
if without_semicolon.ends_with(';') || without_semicolon.trim().is_empty() {
return text.to_string();
}
without_semicolon
.trim_end_matches([' ', '\t', '\r'])
.to_string()
}
fn case_pattern_pipe_alignment_width(text: &str) -> usize {
let Some(close_paren) = text.find(')') else {
return 0;
};
let pattern = &text[..close_paren];
let mut adjustment = 0;
for (index, ch) in pattern.char_indices() {
if ch != '|' {
continue;
}
let previous = pattern[..index].chars().next_back();
if previous == Some('\\') {
continue;
}
if previous.is_some_and(|ch| !ch.is_whitespace() && ch != '|') {
adjustment += 1;
}
if pattern[index + ch.len_utf8()..]
.chars()
.next()
.is_some_and(|ch| !ch.is_whitespace() && ch != '|')
{
adjustment += 1;
}
}
adjustment
}
fn trim_compound_assignment_padding_for_alignment(text: &str) -> String {
let mut normalized = text.to_string();
let Some(open) = normalized.find("=(") else {
return normalized;
};
let body_start = open + 2;
while normalized
.as_bytes()
.get(body_start)
.is_some_and(u8::is_ascii_whitespace)
{
normalized.remove(body_start);
}
let Some(close) = normalized.rfind(')') else {
return normalized;
};
let mut index = close;
while index > body_start
&& normalized
.as_bytes()
.get(index.saturating_sub(1))
.is_some_and(u8::is_ascii_whitespace)
{
normalized.remove(index - 1);
index -= 1;
}
normalized
}
fn moved_function_brace_alignment_width(text: &str) -> usize {
let trimmed = text.trim_end();
if trimmed
.strip_prefix("function ")
.is_some_and(|rest| !rest.trim().is_empty())
{
return 1;
}
usize::from(trimmed.ends_with("()") && !trimmed.ends_with(" ()") && !trimmed.contains('='))
}
fn line_is_skippable_alignment_opener(line: &str) -> bool {
matches!(line.trim_matches([' ', '\t', '\r']), "{" | "(")
}
fn collapse_horizontal_whitespace_runs(text: &str) -> String {
let mut collapsed = String::with_capacity(text.len());
let mut in_horizontal_space = false;
for ch in text.chars() {
if matches!(ch, ' ' | '\t' | '\r') {
if !in_horizontal_space {
collapsed.push(' ');
in_horizontal_space = true;
}
} else {
collapsed.push(ch);
in_horizontal_space = false;
}
}
collapsed
}
fn trim_redirect_padding_for_alignment(text: &str) -> String {
let mut rendered = String::with_capacity(text.len());
let mut last = 0;
let mut index = 0;
let mut in_single_quotes = false;
let mut in_double_quotes = false;
let mut escaped = false;
let bytes = text.as_bytes();
while index < bytes.len() {
let byte = bytes[index];
if byte == b'\'' && !in_double_quotes && !escaped {
in_single_quotes = !in_single_quotes;
index += 1;
continue;
}
if byte == b'"' && !in_single_quotes && !escaped {
in_double_quotes = !in_double_quotes;
index += 1;
continue;
}
if !in_single_quotes && !in_double_quotes && byte.is_ascii_digit() {
let fd_start = index;
let mut operator_start = index + 1;
while operator_start < bytes.len() && bytes[operator_start].is_ascii_digit() {
operator_start += 1;
}
if let Some(operator_end) = redirect_operator_end(bytes, operator_start)
&& let Some(target_start) = redirect_target_start_after_padding(bytes, operator_end)
{
rendered.push_str(&text[last..operator_end]);
last = target_start;
index = target_start;
escaped = false;
continue;
}
index = fd_start;
}
if !in_single_quotes
&& !in_double_quotes
&& matches!(byte, b'<' | b'>')
&& !alignment_operator_is_inside_test_expression(text, index)
&& let Some(operator_end) = redirect_operator_end(bytes, index)
&& let Some(target_start) = redirect_target_start_after_padding(bytes, operator_end)
{
rendered.push_str(&text[last..operator_end]);
last = target_start;
index = target_start;
escaped = false;
continue;
}
if !in_single_quotes
&& !in_double_quotes
&& bytes.get(index..index + 3) == Some(b"<<<")
&& let Some(target_start) = redirect_target_start_after_padding(bytes, index + 3)
{
rendered.push_str(&text[last..index + 3]);
last = target_start;
index = target_start;
escaped = false;
continue;
}
escaped = !in_single_quotes && byte == b'\\' && !escaped;
if byte != b'\\' {
escaped = false;
}
index += 1;
}
rendered.push_str(&text[last..]);
rendered
}
fn alignment_operator_is_inside_test_expression(text: &str, index: usize) -> bool {
let Some(prefix) = text.get(..index) else {
return false;
};
let Some(suffix) = text.get(index..) else {
return false;
};
let inside_conditional = prefix
.rfind("[[")
.is_some_and(|open| !prefix[open + 2..].contains("]]") && suffix.contains("]]"));
let inside_arithmetic = prefix
.rfind("((")
.is_some_and(|open| !prefix[open + 2..].contains("))") && suffix.contains("))"));
inside_conditional || inside_arithmetic
}
fn redirect_target_start_after_padding(bytes: &[u8], operator_end: usize) -> Option<usize> {
let mut target_start = operator_end;
while target_start < bytes.len() && matches!(bytes[target_start], b' ' | b'\t' | b'\r') {
target_start += 1;
}
(target_start > operator_end
&& target_start < bytes.len()
&& !matches!(bytes[target_start], b'<' | b'>' | b'&'))
.then_some(target_start)
}
fn trim_arithmetic_expansion_padding_for_alignment(text: &str) -> String {
let mut rendered = String::with_capacity(text.len());
let mut index = 0;
while index < text.len() {
let rest = &text[index..];
if rest.starts_with("$((") {
rendered.push_str("$((");
index += 3;
while text[index..].starts_with(' ') {
index += 1;
}
continue;
}
if rest.starts_with("$(") {
rendered.push_str("$(");
index += 2;
while text[index..].starts_with(' ') {
index += 1;
}
continue;
}
if rest.starts_with(" ))") {
rendered.push_str("))");
index += 3;
continue;
}
if rest.starts_with(" )") {
rendered.push(')');
index += 2;
continue;
}
let Some(ch) = rest.chars().next() else {
break;
};
rendered.push(ch);
index += ch.len_utf8();
}
rendered
}
fn normalize_empty_parameter_replacements_for_alignment(text: &str) -> String {
normalize_raw_empty_parameter_replacement_delimiters(text).unwrap_or_else(|| text.to_string())
}
fn comment_precedes_close_keyword_at_same_indent(
source: &str,
source_map: &SourceMap<'_>,
comment: &SourceComment<'_>,
) -> bool {
let Some(comment_indent) = source_map.line_indent_before_offset(comment.span().start.offset())
else {
return false;
};
let Some((_, comment_line_end)) =
source_map.line_bounds_for_offset(comment.span().end.offset())
else {
return false;
};
let mut next_line = source_map.next_line_bounds(comment_line_end);
while let Some((line_start, line_end)) = next_line {
let Some(line) = source.get(line_start..line_end) else {
return false;
};
let trimmed = line.trim_start_matches([' ', '\t']);
if trimmed.trim().is_empty() {
next_line = source_map.next_line_bounds(line_end);
continue;
}
let indent_len = line.len() - trimmed.len();
if line.get(..indent_len) == Some(comment_indent) {
if starts_with_outdent_preserving_close_keyword(trimmed) {
return true;
}
if trimmed.starts_with('#') {
next_line = source_map.next_line_bounds(line_end);
continue;
}
}
return false;
}
false
}
fn starts_with_outdent_preserving_close_keyword(text: &str) -> bool {
["fi"].iter().any(|keyword| {
text.strip_prefix(keyword).is_some_and(|rest| {
rest.chars()
.next()
.is_none_or(|ch| !ch.is_ascii_alphanumeric() && ch != '_')
})
})
}