stylua_lib/
context.rs

1use crate::{
2    shape::Shape, BlockNewlineGaps, CallParenType, CollapseSimpleStatement, Config, IndentType,
3    LineEndings, Range as FormatRange, SpaceAfterFunctionNames,
4};
5use full_moon::{
6    node::Node,
7    tokenizer::{Token, TokenType},
8};
9
10#[derive(Debug, PartialEq, Eq)]
11pub enum FormatNode {
12    /// The formatting is completely blocked via an ignore comment, so this node should be skipped
13    Skip,
14    /// This node is outside the range, but we should still look to format internally to find items within the range
15    NotInRange,
16    /// There is no restriction, the node should be formatted normally
17    Normal,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub struct Context {
22    /// The configuration passed to the formatter
23    config: Config,
24    /// An optional range of values to format within the file.
25    range: Option<FormatRange>,
26    /// Whether the formatting has currently been disabled. This should occur when we see the relevant comment.
27    formatting_disabled: bool,
28}
29
30impl Context {
31    /// Creates a new Context, with the given configuration
32    pub fn new(config: Config, range: Option<FormatRange>) -> Self {
33        Self {
34            config,
35            range,
36            formatting_disabled: false,
37        }
38    }
39
40    /// Get the configuration for this context
41    pub fn config(&self) -> Config {
42        self.config
43    }
44
45    /// Determines whether we need to toggle whether formatting is enabled or disabled.
46    /// Formatting is toggled on/off whenever we see a `-- stylua: ignore start` or `-- stylua: ignore end` comment respectively.
47    // To preserve immutability of Context, we return a new Context with the `formatting_disabled` field toggled or left the same
48    // where necessary. Context is cheap so this is reasonable to do.
49    pub fn check_toggle_formatting(&self, node: &impl Node) -> Self {
50        // Load all the leading comments from the token
51        let leading_trivia = node.surrounding_trivia().0;
52        let comment_lines = leading_trivia
53            .iter()
54            .filter_map(|trivia| {
55                match trivia.token_type() {
56                    TokenType::SingleLineComment { comment } => Some(comment),
57                    TokenType::MultiLineComment { comment, .. } => Some(comment),
58                    _ => None,
59                }
60                .map(|comment| comment.lines().map(|line| line.trim()))
61            })
62            .flatten();
63
64        // Load the current formatting disabled state
65        let mut formatting_disabled = self.formatting_disabled;
66
67        // Work through all the lines and update the state as necessary
68        for line in comment_lines {
69            if line == "stylua: ignore start" {
70                formatting_disabled = true;
71            } else if line == "stylua: ignore end" {
72                formatting_disabled = false;
73            }
74        }
75
76        Self {
77            formatting_disabled,
78            ..*self
79        }
80    }
81
82    /// Checks whether we should format the given node.
83    /// Firstly determine if formatting is disabled (due to the relevant comment)
84    /// If not, determine whether the node has an ignore comment present.
85    /// If not, checks whether the provided node is outside the formatting range.
86    /// If not, the node should be formatted.
87    pub fn should_format_node(&self, node: &impl Node) -> FormatNode {
88        // If formatting is disabled we should immediately bailed out.
89        if self.formatting_disabled {
90            return FormatNode::Skip;
91        }
92
93        // Check comments
94        let leading_trivia = node.surrounding_trivia().0;
95        for trivia in leading_trivia {
96            let comment_lines = match trivia.token_type() {
97                TokenType::SingleLineComment { comment } => comment,
98                TokenType::MultiLineComment { comment, .. } => comment,
99                _ => continue,
100            }
101            .lines()
102            .map(|line| line.trim());
103
104            for line in comment_lines {
105                if line == "stylua: ignore" {
106                    return FormatNode::Skip;
107                }
108            }
109        }
110
111        if let Some(range) = self.range {
112            match (range.start, node.start_position()) {
113                (Some(start_bound), Some(node_start)) if node_start.bytes() < start_bound => {
114                    return FormatNode::NotInRange
115                }
116                _ => (),
117            };
118
119            match (range.end, node.end_position()) {
120                (Some(end_bound), Some(node_end)) if node_end.bytes() > end_bound => {
121                    return FormatNode::NotInRange
122                }
123                _ => (),
124            }
125        }
126
127        FormatNode::Normal
128    }
129
130    #[allow(deprecated)]
131    pub fn should_omit_string_parens(&self) -> bool {
132        self.config().no_call_parentheses
133            || self.config().call_parentheses == CallParenType::None
134            || self.config().call_parentheses == CallParenType::NoSingleString
135    }
136
137    #[allow(deprecated)]
138    pub fn should_omit_table_parens(&self) -> bool {
139        self.config().no_call_parentheses
140            || self.config().call_parentheses == CallParenType::None
141            || self.config().call_parentheses == CallParenType::NoSingleTable
142    }
143
144    pub fn should_collapse_simple_functions(&self) -> bool {
145        matches!(
146            self.config().collapse_simple_statement,
147            CollapseSimpleStatement::FunctionOnly | CollapseSimpleStatement::Always
148        )
149    }
150
151    pub fn should_collapse_simple_conditionals(&self) -> bool {
152        matches!(
153            self.config().collapse_simple_statement,
154            CollapseSimpleStatement::ConditionalOnly | CollapseSimpleStatement::Always
155        )
156    }
157
158    pub fn should_preserve_leading_block_newline_gaps(&self) -> bool {
159        matches!(self.config().block_newline_gaps, BlockNewlineGaps::Preserve)
160    }
161
162    pub fn should_preserve_trailing_block_newline_gaps(&self) -> bool {
163        matches!(self.config().block_newline_gaps, BlockNewlineGaps::Preserve)
164    }
165}
166
167/// Returns the relevant line ending string from the [`LineEndings`] enum
168pub fn line_ending_character(line_endings: LineEndings) -> String {
169    match line_endings {
170        LineEndings::Unix => String::from("\n"),
171        LineEndings::Windows => String::from("\r\n"),
172    }
173}
174
175/// Creates a new Token containing whitespace for indents, used for trivia
176pub fn create_indent_trivia(ctx: &Context, shape: Shape) -> Token {
177    let indent_level = shape.indent().block_indent() + shape.indent().additional_indent();
178    create_plain_indent_trivia(ctx, indent_level)
179}
180
181/// Creates indent trivia without including `ctx.indent_level()`.
182/// You should pass the exact amount of indent you require to this function
183pub fn create_plain_indent_trivia(ctx: &Context, indent_level: usize) -> Token {
184    match ctx.config().indent_type {
185        IndentType::Tabs => Token::new(TokenType::tabs(indent_level)),
186        IndentType::Spaces => {
187            Token::new(TokenType::spaces(indent_level * ctx.config().indent_width))
188        }
189    }
190}
191
192/// Creates a new Token containing whitespace used after function declarations
193pub fn create_function_definition_trivia(ctx: &Context) -> Token {
194    match ctx.config().space_after_function_names {
195        SpaceAfterFunctionNames::Always | SpaceAfterFunctionNames::Definitions => {
196            Token::new(TokenType::spaces(1))
197        }
198        SpaceAfterFunctionNames::Never | SpaceAfterFunctionNames::Calls => {
199            Token::new(TokenType::spaces(0))
200        }
201    }
202}
203
204/// Creates a new Token containing whitespace used after function calls
205pub fn create_function_call_trivia(ctx: &Context) -> Token {
206    match ctx.config().space_after_function_names {
207        SpaceAfterFunctionNames::Always | SpaceAfterFunctionNames::Calls => {
208            Token::new(TokenType::spaces(1))
209        }
210        SpaceAfterFunctionNames::Never | SpaceAfterFunctionNames::Definitions => {
211            Token::new(TokenType::spaces(0))
212        }
213    }
214}
215
216/// Creates a new Token containing new line whitespace, used for trivia
217pub fn create_newline_trivia(ctx: &Context) -> Token {
218    Token::new(TokenType::Whitespace {
219        characters: line_ending_character(ctx.config().line_endings).into(),
220    })
221}