Skip to main content

format_node_range

Function format_node_range 

Source
pub fn format_node_range(
    node: &SyntaxNode,
    range: TextRange,
) -> Option<(TextRange, String)>
Expand description

Format the subset of node’s top-level children that intersect range, returning the snapped byte range and the canonical-form replacement text.

This is the building block for the LSP textDocument/rangeFormatting provider: the client sends a Range, the server snaps it up to the smallest set of top-level structural nodes (directives or standalone comments) that intersect the selection, formats those nodes the same way format_node formats the whole file, and returns a single TextEdit replacing the snapped range. The alternative — formatting a substring of the source — would have to either invent a partial canonical form (creating a second truth alongside the whole-file canonical form, the failure mode that bit #1252) or refuse to format anything that crosses a structural boundary. Snapping up to top-level boundaries is the only choice that lets the same canonical-form rules apply.

Frame. range is in the CST byte frame — the same frame the syntax node’s TextRanges use. The LSP handler is responsible for shifting bom_offset at the input/output boundary (mirrors the super::super::SyntaxNode / selection_range handler convention; see ParseResult::syntax_root rustdoc for the rationale).

Behavior.

  • If range intersects no top-level Directive or standalone COMMENT/SHEBANG/EMACS token, returns None. The LSP handler surfaces None directly (serialized as null per LSP, not as []); the client treats it as “nothing to format”.
  • If the computed snap range would cover any top-level ERROR_NODE byte, returns None. Range formatting refuses to delete user content the parser couldn’t classify. This diverges from format_node, which silently drops ERROR_NODE children on the whole-file path; the rationale is the per-handler asymmetry the LSP exposes — the user pressing “Format Selection” expects either a clean reformat or a no-op, never a silent partial delete of an in-progress directive. Tooling that genuinely wants to drop broken regions can still call format_node on the same node.
  • Otherwise returns Some((snap, text)) where snap is the union of the included children’s text ranges (so it begins at the first included child’s start and ends at the last included child’s end, including each child’s leading-trivia prefix per the phase-2.0 Directive-Terminator Rule) and text is the canonical-form replacement.
  • Cursor-only selection (range.is_empty()): the child at the cursor is included if the cursor is strictly inside it OR is exactly at the child’s start. Boundary at the child’s end belongs to the next child, not the previous one — matches the standard “end-of-line cursor is start-of-next-line” convention.

Posting alignment. The pre-pass uses the FULL SourceFile, not the selected subset. A selection that formats one transaction in a file with many other transactions inherits the file’s alignment columns, so the formatted output stays visually aligned with un-formatted postings elsewhere. The opposite policy (per-selection alignment) would create a jarring visual jump every time the user re-formats a sub-range.

Round-trip invariant. For any range that contains every top-level child, the returned text equals the result of format_node on the same node. Pinned by format_node_range_full_range_matches_format_node in this file’s test module.

§Panics

Panics if node’s kind is not SOURCE_FILE — same precondition as format_node.