use std::fmt::Write as _;
use std::mem;
use shuck_ast::{
AlwaysCommand, AnonymousFunctionCommand, ArithmeticCommand, ArithmeticForCommand, ArrayElem,
Assignment, AssignmentValue, BinaryCommand, BinaryOp, BuiltinCommand, CaseCommand, CaseItem,
Command, CompoundCommand, ConditionalBinaryExpr, ConditionalBinaryOp, ConditionalCommand,
ConditionalExpr, ConditionalParenExpr, ConditionalUnaryExpr, ConditionalUnaryOp, CoprocCommand,
DeclClause, DeclOperand, File, ForCommand, ForSyntax, ForeachCommand, ForeachSyntax,
FunctionDef, HeredocBody, HeredocBodyPart, IfCommand, IfSyntax, Pattern, PatternPart, Redirect,
RedirectKind, RepeatCommand, SelectCommand, SimpleCommand, Span, Stmt, StmtSeq, StmtTerminator,
TimeCommand, UntilCommand, VarRef, WhileCommand, Word, WordPart,
};
mod branches;
pub(crate) mod comments_alignment;
mod compounds;
mod gaps;
mod redirects;
mod statements;
mod substitutions;
mod writer;
use crate::Result;
use crate::command::{
array_elem_parts, binary_operator, builtin_like_parts, case_item_body_upper_bound,
case_terminator, collect_binary_list_first as collect_binary_list_first_with,
collect_pipeline_parts, command_format_span, command_group_commands,
format_arithmetic_command_source, format_arithmetic_for_clause_source,
if_next_branch_region_with_body_end, line_gap_break_count, matching_group_close,
multiline_compound_assignment_command_substitution_body_prefix,
multiline_compound_assignment_layout, multiline_compound_assignment_lines,
render_assignment_head_to_buf, render_assignment_to_buf, render_background_operator,
render_subscript_to_buf, render_var_ref_to_buf, simple_command_uses_synthetic_words,
slice_span, stmt_format_span, stmt_span, stmt_start_after_operator,
stmt_verbatim_span_with_source_map, trim_unescaped_trailing_whitespace,
};
use crate::comments::{BranchPrefixComment, SourceComment, SourceMap};
use crate::context::RenderContext;
use crate::facts::FormatterFacts;
use crate::options::{IndentStyle, ResolvedShellFormatOptions};
use crate::raw_syntax::{
CommandSubstitutionPipelineContinuation, RawLineQuoteState, RawShellText, RenderedHeredocTail,
command_substitution_context_start, command_substitution_pipeline_stage_continuation,
line_without_continuation_backslash, normalize_rendered_heredoc_start_spacing,
redirect_operator_end, rendered_heredoc_tail_start,
rendered_line_ends_with_structural_pipe_continuation,
rendered_line_opens_command_substitution_pipeline, rendered_shell_text_has_heredoc_tail,
};
use crate::render_plan::{
CaseCloseLayout, CaseLayoutPlan, CaseLayoutStyle, CompoundBodyLayout, CompoundBodyPlan,
CompoundBodySite, DoDoneBodyLayout, ExpandedIfLayout, FunctionBodyGroupLayout,
FunctionBodyLayout, FunctionBodyPlan, FunctionSubshellLayout, IfLayoutPlan, IfLayoutStyle,
InlineIfLayout, can_format_multiline_subshell_inline, can_inline_group,
can_inline_source_line_subshell, case_can_format_inline, group_has_inline_source_shape,
};
use crate::render_plan::{
case_item_body_can_share_terminator, case_item_body_terminator_was_inline_in_source,
case_item_body_was_inline_without_terminator, case_item_close_paren_shares_line_with_body,
case_item_pattern_body_terminator_was_inline_in_source,
case_item_pattern_close_paren_on_own_line, case_item_single_body_stmt_can_inline,
case_item_started_inline_without_terminator, case_prefix_comment_uses_body_indent,
comment_looks_like_disabled_case_pattern, trim_trailing_pattern_line_continuation,
};
use crate::render_plan::{
condition_keyword_on_previous_non_empty_line, elif_condition_has_explicit_statement_break,
loop_condition_starts_after_keyword, stmt_sequence_renders_with_subshell_open,
};
use crate::source::SourceView;
use crate::word::{
normalize_raw_empty_parameter_replacement_delimiters,
normalize_raw_unquoted_word_continuations, render_arithmetic_expr_to_buf,
render_escaped_multiline_word_syntax_to_buf, render_heredoc_body_to_buf,
render_pattern_syntax_to_buf, render_word_syntax_to_buf,
word_gap_end_before_trailing_continuation, word_is_quoted_command_substitution_only,
word_is_quoted_formattable_command_substitution_only_with_facts,
};
use branches::{
branch_prefix_comments_use_disabled_body_indent, if_branch_upper_bound,
unmodeled_branch_background_operator,
};
use comments_alignment::inline_comment_code_width;
use gaps::{
gap_has_blank_line, stmt_rendered_end_line_after_format,
stmt_semicolon_terminator_starts_on_continuation_line, trim_trailing_gap_before_offset,
};
use redirects::{
append_both_redirect_pair_matches_source, redirect_is_attached_process_substitution,
redirect_list_needs_leading_space, redirect_list_starts_on_continuation_line,
};
use substitutions::{
assignment_source_has_command_substitution, conditional_binary_has_explicit_rhs_break,
conditional_expr_contains_command_substitution, heredoc_body_contains_command_substitution,
interstitial_comment_end, pipeline_operator_breaks, word_contains_command_substitution,
};
use writer::{BufferSink, CompareSink, PendingHeredoc, ShellWriter, StreamSink};
pub(crate) fn format_file_streaming(file: &File, context: RenderContext<'_, '_>) -> Result<String> {
let mut formatter = ShellRenderer::new(context);
formatter.format_stmt_sequence(&file.body, None)?;
Ok(formatter.finish_into_string())
}
pub(crate) fn format_file_streaming_matches_source(
file: &File,
context: RenderContext<'_, '_>,
) -> Result<bool> {
let mut formatter = ShellRenderer::new_compare(context);
formatter.format_stmt_sequence(&file.body, None)?;
Ok(formatter.finish_matches_source())
}
pub(crate) fn format_stmt_sequence_streaming_to_buf(
context: RenderContext<'_, '_>,
statements: &StmtSeq,
upper_bound: Option<usize>,
output: &mut String,
) -> Result<()> {
let mut nested_output = mem::take(output);
nested_output.clear();
let mut formatter = ShellRenderer::with_output_buffer(context, nested_output);
formatter.format_stmt_sequence(statements, upper_bound)?;
*output = formatter.finish_into_string();
Ok(())
}
struct ShellRenderer<'source, 'facts, S> {
source: &'source str,
options: ResolvedShellFormatOptions,
facts: &'facts FormatterFacts<'source>,
scratch: String,
writer: ShellWriter<S>,
pipeline_continuation_indent: usize,
filter_next_group_body_leading_before_open: bool,
}
#[derive(Clone, Copy)]
struct SequenceRenderSnapshot<'source, 'facts> {
facts: &'facts crate::facts::SequenceFacts<'source>,
has_comments: bool,
ambiguous: bool,
}
impl<'source, 'facts> SequenceRenderSnapshot<'source, 'facts> {
fn new(facts: &'facts crate::facts::SequenceFacts<'source>) -> Self {
Self {
facts,
has_comments: facts.has_comments(),
ambiguous: facts.is_ambiguous(),
}
}
fn leading_for(self, index: usize) -> &'facts [SourceComment<'source>] {
self.facts.leading_for(index)
}
fn trailing_for(self, index: usize) -> &'facts [SourceComment<'source>] {
self.facts.trailing_for(index)
}
fn dangling(self) -> &'facts [SourceComment<'source>] {
self.facts.dangling()
}
fn has_comments(self) -> bool {
self.has_comments
}
fn is_ambiguous(self) -> bool {
self.ambiguous
}
fn first_rendered_line_for(self, index: usize) -> usize {
self.facts.first_rendered_line_for(index)
}
}
impl<'source, 'facts> ShellRenderer<'source, 'facts, BufferSink> {
fn new(context: RenderContext<'source, 'facts>) -> Self {
Self::with_writer(
context,
ShellWriter::new_buffer(context.source, context.options),
)
}
fn with_output_buffer(context: RenderContext<'source, 'facts>, output: String) -> Self {
Self::with_writer(
context,
ShellWriter::with_output_buffer(context.options, output),
)
}
fn finish_into_string(self) -> String {
self.writer.finish_into_string()
}
}
impl<'source, 'facts> ShellRenderer<'source, 'facts, CompareSink<'source>> {
fn new_compare(context: RenderContext<'source, 'facts>) -> Self {
Self::with_writer(
context,
ShellWriter::new_compare(context.source, context.options),
)
}
fn finish_matches_source(self) -> bool {
self.writer.finish_matches_source()
}
}
impl<'source, 'facts, S> ShellRenderer<'source, 'facts, S>
where
S: StreamSink,
{
fn with_writer(context: RenderContext<'source, 'facts>, writer: ShellWriter<S>) -> Self {
Self {
source: context.source,
options: context.options.clone(),
facts: context.facts,
scratch: String::new(),
writer,
pipeline_continuation_indent: 1,
filter_next_group_body_leading_before_open: false,
}
}
fn push_output_str(&mut self, text: &str) {
self.writer.push_raw_str(text);
}
fn source(&self) -> &'source str {
self.source
}
fn options(&self) -> &ResolvedShellFormatOptions {
&self.options
}
fn facts(&self) -> &FormatterFacts<'source> {
self.facts
}
fn source_map(&self) -> &SourceMap<'source> {
self.render_context().source_map()
}
fn render_context(&self) -> RenderContext<'source, '_> {
RenderContext::new(self.source(), self.options(), self.facts())
}
fn line_ending(&self) -> &'static str {
self.writer.line_ending()
}
fn indent_column_for_level(&self, level: usize) -> usize {
self.writer.indent_column_for_level(level)
}
fn indent_level(&self) -> usize {
self.writer.indent_level()
}
fn column(&self) -> usize {
self.writer.column()
}
fn line_indent_column(&self) -> usize {
self.writer.line_indent_column()
}
fn line_start(&self) -> bool {
self.writer.line_start()
}
fn with_indent<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
self.writer.push_indent(1);
let result = f(self);
self.writer.pop_indent(1);
result
}
fn take_scratch_buffer(&mut self) -> String {
let mut scratch = mem::take(&mut self.scratch);
scratch.clear();
scratch
}
fn restore_scratch_buffer(&mut self, scratch: String) {
self.scratch = scratch;
}
fn render_word_to_buffer(&self, word: &Word, rendered: &mut String) {
render_word_syntax_to_buf(word, self.render_context(), rendered);
}
fn write_rendered(
&mut self,
render: impl FnOnce(&mut String, &'source str, &ResolvedShellFormatOptions),
) {
let mut scratch = self.take_scratch_buffer();
render(&mut scratch, self.source, &self.options);
self.write_text(&scratch);
self.restore_scratch_buffer(scratch);
}
fn write_display(&mut self, value: impl std::fmt::Display) {
self.write_rendered(|scratch, _, _| {
let _ = write!(scratch, "{value}");
});
}
fn write_indent_units(&mut self, levels: usize) {
self.writer.write_indent_units(levels);
}
fn write_text(&mut self, text: &str) {
self.writer.write_text(text);
}
fn write_verbatim(&mut self, text: &str) {
self.writer.write_verbatim(text);
}
fn write_indent(&mut self) {
self.writer.write_indent();
}
fn write_indent_to_column(&mut self, column: usize) {
self.writer.write_indent_to_column(column);
}
fn write_space(&mut self) {
self.writer.write_space();
}
fn write_spaces(&mut self, count: usize) {
self.writer.write_spaces(count);
}
fn flush_pending_heredocs(&mut self) {
self.writer.flush_pending_heredocs();
}
fn newline(&mut self) {
self.writer.newline();
}
fn line_continuation(&mut self) {
self.writer.line_continuation();
}
fn write_line_breaks(&mut self, count: usize) {
self.writer.write_line_breaks(count);
}
}
fn split_first_line(text: &str) -> (&str, &str, bool) {
text.split_once('\n')
.map_or((text, "", false), |(line, rest)| (line, rest, true))
}
fn split_first_line_including_newline(text: &str) -> (&str, &str, bool) {
text.find('\n').map_or((text, "", false), |index| {
let (line, next) = text.split_at(index + 1);
(line, next, true)
})
}
fn last_shell_keyword_span(
source: &str,
source_map: &SourceMap<'_>,
span: Span,
keyword: &str,
) -> Option<Span> {
let start = SourceView::new(source).last_shell_keyword_start(span, keyword)?;
Some(source_map.span_for_offsets(start, start + keyword.len()))
}