radFormatter / docs / known limitations
Known limitations
Known constraints and deliberate limits for radFormatter v2. Some of these are engineering trade-offs; others are principled decisions about what a layout formatter should and should not do. They are documented here so behavior is predictable rather than surprising.
AsIs = preserve the source
as-is for any spacing option means do not alter the
source spacing — the original inter-token whitespace (including
multiple spaces and tabs) is reproduced verbatim. Normalization happens only
where an option is set to a non-AsIs value, and only for that option's gap
type.
Tabs and indent style
indent.style extends the same “preserve source by
default” stance to the indent character. At the default
profile it is asIs: the formatter detects the source's indent
character (the first ordinary indented line decides — a leading tab
→ tabs, a leading space → spaces; none → spaces) and
preserves it, rather than silently converting tab-indented source to spaces.
Only the character is preserved — indent depth is always
recomputed. The opinionated profiles set
style: "spaces".
Consequently indent.size is a spaces-only
setting: under tabs the tab count is the structural level and the editor's
tab stop owns the visual width, so indent.size does not apply.
Continuation indent and alignment padding stay spaces even
under tabs (“smart tabs”). Right-margin measurement counts a
leading-indent tab at its true visual tab-stop width, so soft-break
decisions on tab-indented lines use the correct column.
Conditional compilation
Inactive conditional branches are treated as preserve barriers — their content is reproduced untouched. Rich directive formatting is limited to indent (left margin vs structural depth) and casing. Formatting never joins, splits, aligns, or wraps through conditional boundaries.
Right margin scope
Right-margin wrapping works by activating declared soft break points, so coverage depends on which constructs declare them. Currently supported wrap points:
- Call arguments
- Formal parameter lists
usesclause commas- Binary expression operators
- Record initializer fields
- Class/interface inheritance lists
- Type member declaration boundaries
- Enum values
- Array initializer elements
- Index, parenthesized, and set expressions
Iterative multi-wrap is supported: after a wrap, indentation is recomputed and over-long lines are re-measured.
Not yet supported for wrapping:
- Consecutive statements in
begin..endblocks - Consecutive
var/constsection declarations - The assignment operator (
:=) as a wrap point - Long string literals (splitting would require synthesizing
' + 'concatenation tokens)
Comment indentation
An own-line comment is aligned to the innermost block of code still
open at its position, rather than computing an indent of its own.
In practice the formatter matches the comment to a nearby statement in the
same block; for an empty block it uses the block's opener plus one indent
level; and a comment that is the empty or leading body of an unbraced
if / for / while / with
branch — sitting directly after then, else,
or do — takes that branch's body column. A comment that
follows a block's own closing end returns to the enclosing
level. Only when none of these apply does it fall back to matching the
next non-comment line.
So a comment inside a block indents with the block body, not with the dedenting closer that follows it:
begin
if AOptions.UsesMode = umOnePerLine then
begin
...
end;
// umSingleLine handled below via join logic. <- block-body level, not the outer 'end'
end;
and a placeholder comment in an otherwise-empty then branch
takes the deeper then-body column, not the else:
if (...) then
// { skip -- option explicitly requests collapsing } <- then-body column
else
IsStructural := True;
Residual limitation
One narrow ambiguity remains. A comment sitting between a
completed statement and a following dedenting closer
(else, end, until,
finally) falls back to the next line, so it takes the closer's
shallower level rather than the deeper statement it might be trailing:
if X then
A
// prose introducing the else <- 'else' level, not A's deeper level
else
B;
This is a deliberate default: position alone cannot tell
“trailing the statement above” from “introducing the
construct below,” and the two intents want opposite indents. Aligning
such a comment to the deeper preceding line would break the common case of a
comment that heads an else / until /
finally, so the predictable next-line default is kept for the
cases the block-relative rules cannot resolve.
Alignment
Alignment runs after indent and owns only interior padding. Alignment interacts correctly with right-margin wrapping — alignment is computed before wrapping, and wrapping triggers indent recomputation but not alignment recomputation.