use tracing::Level; use ruff_formatter::printer::SourceMapGeneration; use ruff_formatter::{ format, FormatContext, FormatError, FormatOptions, IndentStyle, PrintedRange, SourceCode, }; use ruff_python_ast::visitor::source_order::{walk_body, SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::{AnyNodeRef, Stmt, StmtMatch, StmtTry}; use ruff_python_parser::{parse, AsMode}; use ruff_python_trivia::{ indentation_at_offset, BackwardsTokenizer, CommentRanges, SimpleToken, SimpleTokenKind, }; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::comments::Comments; use crate::context::{IndentLevel, NodeLevel}; use crate::prelude::*; use crate::statement::suite::DocstringStmt; use crate::verbatim::{ends_suppression, starts_suppression}; use crate::{format_module_source, FormatModuleError, PyFormatOptions}; /// Formats the given `range` in source rather than the entire file. /// /// The returned formatted range guarantees to cover at least `range` (excluding whitespace), but the range might be larger. /// Some cases in which the returned range is larger than `range` are: /// * The logical lines in `range` use a indentation different from the configured [`IndentStyle`] and [`IndentWidth`]. /// * `range` is smaller than a logical lines and the formatter needs to format the entire logical line. /// * `range` falls on a single line body. /// /// The formatting of logical lines using range formatting should produce the same result as when formatting the entire document (for the same lines and options). /// /// ## Implementation /// /// This is an optimisation problem. The goal is to find the minimal range that fully covers `range`, is still formattable, /// and produces the same result as when formatting the entire document. /// /// The implementation performs the following steps: /// 1. Find the deepest node that fully encloses `range`. The node with the minimum covering range. /// 2. Try to narrow the range found in step one by searching its children and find node and comment start and end offsets that are closer to `range`'s start and end. /// 3. Format the node from step 1 and use the source map information generated by the formatter to map the narrowed range in the source document to the range in the formatted output. /// 4. Take the formatted code and return it. /// /// # Error /// Returns a range error if `range` lies outside of the source file. /// /// # Panics /// If `range` doesn't point to a valid char boundaries. /// /// [`IndentWidth`]: `ruff_formatter::IndentWidth` #[tracing::instrument(name = "format_range", level = Level::TRACE, skip_all)] pub fn format_range( source: &str, range: TextRange, options: PyFormatOptions, ) -> Result { // Error if the specified range lies outside of the source file. if source.text_len() < range.end() { return Err(FormatModuleError::FormatError(FormatError::RangeError { input: range, tree: TextRange::up_to(source.text_len()), })); } // Formatting an empty string always yields an empty string. Return directly. if range.is_empty() { return Ok(PrintedRange::empty()); } if range == TextRange::up_to(source.text_len()) { let formatted = format_module_source(source, options)?; return Ok(PrintedRange::new(formatted.into_code(), range)); } assert_valid_char_boundaries(range, source); let parsed = parse(source, options.source_type().as_mode())?; let source_code = SourceCode::new(source); let comment_ranges = CommentRanges::from(parsed.tokens()); let comments = Comments::from_ast(parsed.syntax(), source_code, &comment_ranges); let mut context = PyFormatContext::new( options.with_source_map_generation(SourceMapGeneration::Enabled), source, comments, parsed.tokens(), ); let (enclosing_node, base_indent) = match find_enclosing_node(range, AnyNodeRef::from(parsed.syntax()), &context) { EnclosingNode::Node { node, indent_level } => (node, indent_level), EnclosingNode::Suppressed => { // The entire range falls into a suppressed range. There's nothing to format. return Ok(PrintedRange::empty()); } }; let narrowed_range = narrow_range(range, enclosing_node, &context); assert_valid_char_boundaries(narrowed_range, source); // Correctly initialize the node level for the blank line rules. if !enclosing_node.is_mod_module() { context.set_node_level(NodeLevel::CompoundStatement); context.set_indent_level( // Plus 1 because `IndentLevel=0` equals the module level. IndentLevel::new(base_indent.saturating_add(1)), ); } let formatted = format!( context, [FormatEnclosingNode { root: enclosing_node }] )?; let printed = formatted.print_with_indent(base_indent)?; Ok(printed.slice_range(narrowed_range, source)) } /// Finds the node with the minimum covering range of `range`. /// /// It traverses the tree and returns the deepest node that fully encloses `range`. /// /// ## Eligible nodes /// The search is restricted to nodes that mark the start of a logical line to ensure /// formatting a range results in the same formatting for that logical line as when formatting the entire document. /// This property can't be guaranteed when supporting sub-expression formatting because /// a) Adding parentheses around enclosing expressions can toggle an expression from non-splittable to splittable, /// b) formatting a sub-expression has fewer split points than formatting the entire expressions. /// /// ### Possible docstrings /// Strings that are suspected to be docstrings are excluded from the search to format the enclosing suite instead /// so that the formatter's docstring detection in [`FormatSuite`] correctly detects and formats the docstrings. /// /// ### Compound statements with a simple statement body /// Don't include simple-statement bodies of compound statements `if True: pass` because the formatter /// must run [`FormatClauseBody`] to determine if the body should be collapsed or not. /// /// ### Incorrectly indented code /// Code that uses indentations that don't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search, /// because formatting such nodes on their own can lead to indentation mismatch with its sibling nodes. /// /// ## Suppression comments /// The search ends when `range` falls into a suppressed range because there's nothing to format. It also avoids that the /// formatter formats the statement because it doesn't see the suppression comment of the enclosing node. /// /// The implementation doesn't handle `fmt: ignore` suppression comments because the statement's formatting logic /// correctly detects the suppression comment and returns the statement text as is. fn find_enclosing_node<'ast>( range: TextRange, root: AnyNodeRef<'ast>, context: &PyFormatContext<'ast>, ) -> EnclosingNode<'ast> { let mut visitor = FindEnclosingNode::new(range, context); if visitor.enter_node(root).is_traverse() { root.visit_preorder(&mut visitor); } visitor.leave_node(root); visitor.closest } struct FindEnclosingNode<'a, 'ast> { range: TextRange, context: &'a PyFormatContext<'ast>, /// The, to this point, deepest node that fully encloses `range`. closest: EnclosingNode<'ast>, /// Tracks if the current statement is suppressed suppressed: Suppressed, } impl<'a, 'ast> FindEnclosingNode<'a, 'ast> { fn new(range: TextRange, context: &'a PyFormatContext<'ast>) -> Self { Self { range, context, suppressed: Suppressed::No, closest: EnclosingNode::Suppressed, } } } impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> { fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal { if !(is_logical_line(node) || node.is_mod_module()) { return TraversalSignal::Skip; } // Handle `fmt: off` suppression comments for statements. if node.is_statement() { let leading_comments = self.context.comments().leading(node); self.suppressed = Suppressed::from(match self.suppressed { Suppressed::No => starts_suppression(leading_comments, self.context.source()), Suppressed::Yes => !ends_suppression(leading_comments, self.context.source()), }); } if !node.range().contains_range(self.range) { return TraversalSignal::Skip; } if self.suppressed.is_yes() && node.is_statement() { self.closest = EnclosingNode::Suppressed; return TraversalSignal::Skip; } // Don't pick potential docstrings as the closest enclosing node because `suite.rs` than fails to identify them as // docstrings and docstring formatting won't kick in. // Format the enclosing node instead and slice the formatted docstring from the result. let is_maybe_docstring = node.as_stmt_expr().is_some_and(|stmt| { DocstringStmt::is_docstring_statement(stmt, self.context.options().source_type()) }); if is_maybe_docstring { return TraversalSignal::Skip; } // Only computing the count here is sufficient because each enclosing node ensures that it has the necessary indent // or we don't traverse otherwise. let Some(indent_level) = indent_level(node.start(), self.context.source(), self.context.options()) else { // Non standard indent or a simple-statement body of a compound statement, format the enclosing node return TraversalSignal::Skip; }; self.closest = EnclosingNode::Node { node, indent_level }; TraversalSignal::Traverse } fn leave_node(&mut self, node: AnyNodeRef<'ast>) { if node.is_statement() { let trailing_comments = self.context.comments().trailing(node); // Update the suppressed state for the next statement. self.suppressed = Suppressed::from(match self.suppressed { Suppressed::No => starts_suppression(trailing_comments, self.context.source()), Suppressed::Yes => !ends_suppression(trailing_comments, self.context.source()), }); } } fn visit_body(&mut self, body: &'ast [Stmt]) { // We only visit statements that aren't suppressed that's why we don't need to track the suppression // state in a stack. Assert that this assumption is safe. debug_assert!(self.suppressed.is_no()); walk_body(self, body); self.suppressed = Suppressed::No; } } #[derive(Debug, Copy, Clone)] enum EnclosingNode<'a> { /// The entire range falls into a suppressed `fmt: off` range. Suppressed, /// The node outside of a suppression range that fully encloses the searched range. Node { node: AnyNodeRef<'a>, indent_level: u16, }, } /// Narrows the formatting `range` to a smaller sub-range than the enclosing node's range. /// /// The range is narrowed by searching the enclosing node's children and: /// * Find the closest node or comment start or end offset to `range.start` /// * Find the closest node or comment start or end offset, or the clause header's `:` end offset to `range.end` /// /// The search is restricted to positions where the formatter emits source map entries because it guarantees /// that we know the exact range in the formatted range and not just an approximation that could include other tokens. /// /// ## Clause Headers /// For clause headers like `if`, `while`, `match`, `case` etc. consider the `:` end position for narrowing `range.end` /// to support formatting the clause header without its body. /// /// ## Compound statements with simple statement bodies /// Similar to [`find_enclosing_node`], exclude the compound statement's body if it is a simple statement (not a suite) from the search to format the entire clause header /// with the body. This ensures that the formatter runs [`FormatClauseBody`] that determines if the body should be indented.s /// /// ## Non-standard indentation /// Node's that use an indentation that doesn't match the configured [`IndentStyle`] and [`IndentWidth`] are excluded from the search. /// This is because the formatter always uses the configured [`IndentStyle`] and [`IndentWidth`], resulting in the /// formatted nodes using a different indentation than the unformatted sibling nodes. This would be tolerable /// in non whitespace sensitive languages like JavaScript but results in lexical errors in Python. /// /// ## Implementation /// It would probably be possible to merge this visitor with [`FindEnclosingNode`] but they are separate because /// it avoids some unnecessary work for nodes that aren't the `enclosing_node` and I found reasoning /// and debugging the visiting logic easier when they are separate. /// /// [`IndentStyle`]: ruff_formatter::IndentStyle /// [`IndentWidth`]: ruff_formatter::IndentWidth fn narrow_range( range: TextRange, enclosing_node: AnyNodeRef, context: &PyFormatContext, ) -> TextRange { let locator = context.locator(); let enclosing_indent = indentation_at_offset(enclosing_node.start(), &locator) .expect("Expected enclosing to never be a same line body statement."); let mut visitor = NarrowRange { context, range, narrowed_start: enclosing_node.start(), narrowed_end: enclosing_node.end(), enclosing_indent, level: usize::from(!enclosing_node.is_mod_module()), }; if visitor.enter_node(enclosing_node).is_traverse() { enclosing_node.visit_preorder(&mut visitor); } visitor.leave_node(enclosing_node); TextRange::new(visitor.narrowed_start, visitor.narrowed_end) } struct NarrowRange<'a> { context: &'a PyFormatContext<'a>, // The range to format range: TextRange, // The narrowed range narrowed_start: TextSize, narrowed_end: TextSize, // Stated tracked by the visitor enclosing_indent: &'a str, level: usize, } impl SourceOrderVisitor<'_> for NarrowRange<'_> { fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal { if !(is_logical_line(node) || node.is_mod_module()) { return TraversalSignal::Skip; } // Find the start offset of the node that starts the closest to (and before) the start offset of the formatting range. // We do this by iterating over known positions that emit source map entries and pick the start point that ends closest // to the searched range's start. let leading_comments = self.context.comments().leading(node); self.narrow(leading_comments); self.narrow([node]); // Avoid traversing when it's known to not be able to narrow the range further to avoid traversing the entire tree (entire file in the worst case). // If the node's range is entirely before the searched range, don't traverse because non of its children // can be closer to `narrow_start` than the node itself (which we already narrowed). // // Don't traverse if the current node is passed the narrowed range (it's impossible to refine it further). if node.end() < self.range.start() || (self.narrowed_start > node.start() && self.narrowed_end <= node.end()) { return TraversalSignal::Skip; } // Handle nodes that have indented child-nodes that aren't a `Body` (which is handled by `visit_body`). // Ideally, this would be handled as part of `visit_stmt` but `visit_stmt` doesn't get called for the `enclosing_node` // because it's not possible to convert` AnyNodeRef` to `&Stmt` :( match node { AnyNodeRef::StmtMatch(StmtMatch { subject: _, cases, range: _, }) => { if let Some(saved_state) = self.enter_level(cases.first().map(AnyNodeRef::from)) { for match_case in cases { self.visit_match_case(match_case); } self.leave_level(saved_state); } // Already traversed as part of `enter_node`. TraversalSignal::Skip } AnyNodeRef::StmtTry(StmtTry { body, handlers, orelse, finalbody, is_star: _, range: _, }) => { self.visit_body(body); if let Some(except_handler_saved) = self.enter_level(handlers.first().map(AnyNodeRef::from)) { for except_handler in handlers { self.visit_except_handler(except_handler); } self.leave_level(except_handler_saved); } self.visit_body(orelse); self.visit_body(finalbody); // Already traversed as part of `enter_node`. TraversalSignal::Skip } _ => TraversalSignal::Traverse, } } fn leave_node(&mut self, node: AnyNodeRef<'_>) { if !(is_logical_line(node) || node.is_mod_module()) { return; } // Find the end offset of the closest node to the end offset of the formatting range. // We do this by iterating over end positions that we know generate source map entries end pick the end // that ends closest or after the searched range's end. self.narrow( self.context .comments() .trailing(node) .iter() .filter(|comment| comment.line_position().is_own_line()), ); } fn visit_body(&mut self, body: &'_ [Stmt]) { if let Some(saved_state) = self.enter_level(body.first().map(AnyNodeRef::from)) { walk_body(self, body); self.leave_level(saved_state); } } } impl NarrowRange<'_> { fn narrow(&mut self, items: I) where I: IntoIterator, T: Ranged, { for ranged in items { self.narrow_offset(ranged.start()); self.narrow_offset(ranged.end()); } } fn narrow_offset(&mut self, offset: TextSize) { self.narrow_start(offset); self.narrow_end(offset); } fn narrow_start(&mut self, offset: TextSize) { if offset <= self.range.start() { self.narrowed_start = self.narrowed_start.max(offset); } } fn narrow_end(&mut self, offset: TextSize) { if offset >= self.range.end() { self.narrowed_end = self.narrowed_end.min(offset); } } fn enter_level(&mut self, first_child: Option) -> Option { if let Some(first_child) = first_child { // If this is a clause header and the `range` ends within the clause header, then avoid formatting the body. // This prevents that we format an entire function definition when the selected range is fully enclosed by the parameters. // ```python // 1| def foo(a, b, c): // 2| pass // ``` // We don't want to format the body of the function. if let Some(SimpleToken { kind: SimpleTokenKind::Colon, range: colon_range, }) = BackwardsTokenizer::up_to( first_child.start(), self.context.source(), self.context.comments().ranges(), ) .skip_trivia() .next() { self.narrow_offset(colon_range.end()); } // It is necessary to format all statements if the statement or any of its parents don't use the configured indentation. // ```python // 0| def foo(): // 1| if True: // 2| print("Hello") // 3| print("More") // 4| a = 10 // ``` // Here, the `if` statement uses the correct 4 spaces indentation, but the two `print` statements use a 2 spaces indentation. // The formatter output uses 8 space indentation for the `print` statement which doesn't match the indentation of the statement on line 4 when // replacing the source with the formatted code. That's why we expand the range in this case to cover the entire if-body range. // // I explored the alternative of using `indent(dedent(formatted))` to retain the correct indentation. It works pretty well except that it can change the // content of multiline strings: // ```python // def test (): // pass // 1 + 2 // """A Multiline string // that uses the same indentation as the formatted code will. This should not be dedented.""" // // print("Done") // ``` // The challenge here is that the second line of the multiline string uses a 4 space indentation. Using `dedent` would // dedent the second line to 0 spaces and the `indent` then adds a 2 space indentation to match the indentation in the source. // This is incorrect because the leading whitespace is the content of the string and not indentation, resulting in changed string content. if let Some(indentation) = indentation_at_offset(first_child.start(), &self.context.locator()) { let relative_indent = indentation.strip_prefix(self.enclosing_indent).unwrap(); let expected_indents = self.level; // Each level must always add one level of indent. That's why an empty relative indent to the parent node tells us that the enclosing node is the Module. let has_expected_indentation = match self.context.options().indent_style() { IndentStyle::Tab => { relative_indent.len() == expected_indents && relative_indent.chars().all(|c| c == '\t') } IndentStyle::Space => { relative_indent.len() == expected_indents * self.context.options().indent_width().value() as usize && relative_indent.chars().all(|c| c == ' ') } }; if !has_expected_indentation { return None; } } else { // Simple-statement body of a compound statement (not a suite body). // Don't narrow the range because the formatter must run `FormatClauseBody` to determine if the body should be collapsed or not. return None; } } let saved_level = self.level; self.level += 1; Some(SavedLevel { level: saved_level }) } #[allow(clippy::needless_pass_by_value)] fn leave_level(&mut self, saved_state: SavedLevel) { self.level = saved_state.level; } } pub(crate) const fn is_logical_line(node: AnyNodeRef) -> bool { // Make sure to update [`FormatEnclosingLine`] when changing this. node.is_statement() || node.is_decorator() || node.is_except_handler() || node.is_elif_else_clause() || node.is_match_case() } #[derive(Debug)] struct SavedLevel { level: usize, } #[derive(Copy, Clone, Default, Debug)] enum Suppressed { /// Code is not suppressed #[default] No, /// The node is suppressed by a suppression comment in the same body block. Yes, } impl Suppressed { const fn is_no(self) -> bool { matches!(self, Suppressed::No) } const fn is_yes(self) -> bool { matches!(self, Suppressed::Yes) } } impl From for Suppressed { fn from(value: bool) -> Self { if value { Suppressed::Yes } else { Suppressed::No } } } fn assert_valid_char_boundaries(range: TextRange, source: &str) { assert!(source.is_char_boundary(usize::from(range.start()))); assert!(source.is_char_boundary(usize::from(range.end()))); } struct FormatEnclosingNode<'a> { root: AnyNodeRef<'a>, } impl Format> for FormatEnclosingNode<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { // Note: It's important that this supports formatting all nodes for which `is_logical_line` // returns + the root `Mod` nodes. match self.root { AnyNodeRef::ModModule(node) => node.format().fmt(f), AnyNodeRef::ModExpression(node) => node.format().fmt(f), AnyNodeRef::StmtFunctionDef(node) => node.format().fmt(f), AnyNodeRef::StmtClassDef(node) => node.format().fmt(f), AnyNodeRef::StmtReturn(node) => node.format().fmt(f), AnyNodeRef::StmtDelete(node) => node.format().fmt(f), AnyNodeRef::StmtTypeAlias(node) => node.format().fmt(f), AnyNodeRef::StmtAssign(node) => node.format().fmt(f), AnyNodeRef::StmtAugAssign(node) => node.format().fmt(f), AnyNodeRef::StmtAnnAssign(node) => node.format().fmt(f), AnyNodeRef::StmtFor(node) => node.format().fmt(f), AnyNodeRef::StmtWhile(node) => node.format().fmt(f), AnyNodeRef::StmtIf(node) => node.format().fmt(f), AnyNodeRef::StmtWith(node) => node.format().fmt(f), AnyNodeRef::StmtMatch(node) => node.format().fmt(f), AnyNodeRef::StmtRaise(node) => node.format().fmt(f), AnyNodeRef::StmtTry(node) => node.format().fmt(f), AnyNodeRef::StmtAssert(node) => node.format().fmt(f), AnyNodeRef::StmtImport(node) => node.format().fmt(f), AnyNodeRef::StmtImportFrom(node) => node.format().fmt(f), AnyNodeRef::StmtGlobal(node) => node.format().fmt(f), AnyNodeRef::StmtNonlocal(node) => node.format().fmt(f), AnyNodeRef::StmtExpr(node) => node.format().fmt(f), AnyNodeRef::StmtPass(node) => node.format().fmt(f), AnyNodeRef::StmtBreak(node) => node.format().fmt(f), AnyNodeRef::StmtContinue(node) => node.format().fmt(f), AnyNodeRef::StmtIpyEscapeCommand(node) => node.format().fmt(f), AnyNodeRef::ExceptHandlerExceptHandler(node) => node.format().fmt(f), AnyNodeRef::MatchCase(node) => node.format().fmt(f), AnyNodeRef::Decorator(node) => node.format().fmt(f), AnyNodeRef::ElifElseClause(node) => node.format().fmt(f), AnyNodeRef::ExprBoolOp(_) | AnyNodeRef::ExprNamed(_) | AnyNodeRef::ExprBinOp(_) | AnyNodeRef::ExprUnaryOp(_) | AnyNodeRef::ExprLambda(_) | AnyNodeRef::ExprIf(_) | AnyNodeRef::ExprDict(_) | AnyNodeRef::ExprSet(_) | AnyNodeRef::ExprListComp(_) | AnyNodeRef::ExprSetComp(_) | AnyNodeRef::ExprDictComp(_) | AnyNodeRef::ExprGenerator(_) | AnyNodeRef::ExprAwait(_) | AnyNodeRef::ExprYield(_) | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) | AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_) | AnyNodeRef::FStringFormatSpec(_) | AnyNodeRef::ExprFString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) | AnyNodeRef::ExprBooleanLiteral(_) | AnyNodeRef::ExprNoneLiteral(_) | AnyNodeRef::ExprEllipsisLiteral(_) | AnyNodeRef::ExprAttribute(_) | AnyNodeRef::ExprSubscript(_) | AnyNodeRef::ExprStarred(_) | AnyNodeRef::ExprName(_) | AnyNodeRef::ExprList(_) | AnyNodeRef::ExprTuple(_) | AnyNodeRef::ExprSlice(_) | AnyNodeRef::ExprIpyEscapeCommand(_) | AnyNodeRef::FString(_) | AnyNodeRef::StringLiteral(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) | AnyNodeRef::PatternMatchMapping(_) | AnyNodeRef::PatternMatchClass(_) | AnyNodeRef::PatternMatchStar(_) | AnyNodeRef::PatternMatchAs(_) | AnyNodeRef::PatternMatchOr(_) | AnyNodeRef::PatternArguments(_) | AnyNodeRef::PatternKeyword(_) | AnyNodeRef::Comprehension(_) | AnyNodeRef::Arguments(_) | AnyNodeRef::Parameters(_) | AnyNodeRef::Parameter(_) | AnyNodeRef::ParameterWithDefault(_) | AnyNodeRef::Keyword(_) | AnyNodeRef::Alias(_) | AnyNodeRef::WithItem(_) | AnyNodeRef::TypeParams(_) | AnyNodeRef::TypeParamTypeVar(_) | AnyNodeRef::TypeParamTypeVarTuple(_) | AnyNodeRef::TypeParamParamSpec(_) | AnyNodeRef::BytesLiteral(_) => { panic!("Range formatting only supports formatting logical lines") } } } } /// Computes the level of indentation for `indentation` when using the configured [`IndentStyle`] and [`IndentWidth`]. /// /// Returns `None` if the indentation doesn't conform to the configured [`IndentStyle`] and [`IndentWidth`]. /// /// # Panics /// If `offset` is outside of `source`. fn indent_level(offset: TextSize, source: &str, options: &PyFormatOptions) -> Option { let locator = Locator::new(source); let indentation = indentation_at_offset(offset, &locator)?; let level = match options.indent_style() { IndentStyle::Tab => { if indentation.chars().all(|c| c == '\t') { Some(indentation.len()) } else { None } } IndentStyle::Space => { let indent_width = options.indent_width().value() as usize; if indentation.chars().all(|c| c == ' ') && indentation.len() % indent_width == 0 { Some(indentation.len() / indent_width) } else { None } } }; level.map(|level| u16::try_from(level).unwrap_or(u16::MAX)) }