Today. AlignAssign aligns every := to one fixed column, AlignAssignPos. Each assignment is padded on its own (TAlignExpression.GetLength pads to FAlignPos when the left side is shorter, and adds nothing when it is longer), so:
Result := dvDeletable;case, one statement per branch;Proposal. A second alignment mode, where each run of consecutive assignments is aligned to its own longest left side:
// block mode
FRepeated := False;
FDeactivated := False;
Caption := DialogControl.GetString( lsSpelling );
Result := Base; // a lone assignment is left alone
NotFoundText.Caption := DialogControl.GetString( lsDlgNotFound );
SuggestionsText.Caption := DialogControl.GetString( lsDlgSuggestions );
Suggested rules (open to your judgement, of course):
AlignAssign becomes a mode: off, fixed column (today's behaviour, the default, so existing configurations format exactly as before) and block. A stored boolean maps as False = off and True = fixed column, as IndentCompDirectives did.AlignAssignPos could serve as an upper limit: a block whose longest left side exceeds it keeps one space throughout, rather than aligning to a far-right column.for loop's :=, and var/const sections, which have their own setting. AlignVar / AlignVarPos could take the same block mode later, but that is a separate change.Suggested implementation. The engine already has the right shape for it. Every aligned := is a
TAlignExpression (put there by AlignExpression in the rtAssignOper branch), and its padding is only worked out late, in CheckWrapping, as a side effect of GetLength against the running line length. So the block mode needs no change to the main formatting loop. It needs one pass, after the token list is complete and before CheckWrapping, which gives each TAlignExpression its block's column instead of AlignAssignPos:
Settings (GX_CodeFormatterSettings.pas): TAlignAssignMode = (aamOff, aamFixedColumn, aamBlock) in place of the AlignAssign boolean, read with the boolean mapping above (GX_CodeFormatterConfigHandler.pas), and on the options page a per block choice beside chk_AlignAssign (GX_CodeFormatterConfig.dfm). AlignAssignPos stays, as the cap in block mode.
Token (GX_CodeFormatterTokens.pas): a writable AlignPos property on TAlignExpression. With AlignPos = 0, GetLength already does the right thing for a single space: _Length < 0 is never true, so it adds no padding and the token keeps its normal spacing.
The pass, AlignAssignBlocks, called from the end of the main routine when the mode is aamBlock, just before if FSettings.WrapLines or FHasAligned then CheckWrapping;:
procedure TCodeFormatterFormatter.AlignAssignBlocks;
// Lines are runs of tokens between rtLineFeed tokens; a blank line is two
// line feeds in a row. For each line that STARTS with an assignment
// (its first TAlignExpression is an rtAssignOper, and only identifiers,
// dots, brackets and their contents stand before it), measure the
// natural length of the left side with AlignPos = 0, exactly as
// CheckWrapping would.
var
Lines: TList<TAssignLine>; // TAssignLine = record Expr: TAlignExpression; LhsEnd, Indent, LineIdx: Integer end
BlockStart, i, Col: Integer;
begin
Lines := CollectAssignLines; // one entry per assignment-starting line, in order
try
BlockStart := 0;
for i := 1 to Lines.Count do
// a block ends at a gap in line numbers (a blank, comment or other
// line between), or at a change of indentation
if (i = Lines.Count)
or (Lines[i].LineIdx <> Lines[i - 1].LineIdx + 1)
or (Lines[i].Indent <> Lines[BlockStart].Indent) then begin
if i - BlockStart > 1 then begin
Col := MaxLhsEnd(Lines, BlockStart, i - 1) + 1; // one space after the longest left side
if Col > FSettings.AlignAssignPos then
Col := 0; // over the cap: single spaces
end else
Col := 0; // a block of one is not padded
SetAlignPos(Lines, BlockStart, i - 1, Col);
BlockStart := i;
end;
finally
Lines.Free;
end;
end;
CollectAssignLines walks FTokens once with a running LineLen, exactly like CheckWrapping. A wrapped right-hand side is a continuation line with no assignment of its own, so in this sketch it ends the block.
Treating it as part of its statement (look back for the statement's ;) would be a refinement.
CheckWrapping already shortens a TAlignExpression's padding when a line runs past WrapPosition.Untested: I have not written or run this against trunk, so take it as a design sketch. I am happy to turn it into a patch with test files in the existing formatter test set if you would take one; or to leave the design entirely to you.
Perspective / provenance: this comes from a Delphi 13 (RAD Studio 37.0), Win32 + Win64 only point of view, which is all we use. The behaviour described is read from trunk r6031 and seen in Ctrl+D output in D13. Theproposal itself is version-neutral, but I have not run anything against an earlier IDE.
Diff: