From 28136d43d0f53f7b5119a6bde4843ef05ab9c869 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Sun, 16 Nov 2025 16:11:11 -0700 Subject: [PATCH 1/3] Improve comment interleaving layout --- .../TsqlFormatterCommentInterleaver.cs | 195 +++++++++++------- 1 file changed, 121 insertions(+), 74 deletions(-) diff --git a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs index 9a6e964..174f455 100644 --- a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs +++ b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs @@ -53,17 +53,25 @@ public static string InterleaveComments(TSqlFragment originalFragment, TSqlFragm mapOrigToFmt[origCodeIdx[iOrigKey]] = fmtCodeIdx[iFmtKey]; } - // Collect comments from original and schedule injection BEFORE a formatted code token. - var injectBeforeFmtIndex = new Dictionary>(); - var eofComments = new List(); - - // Helper to add a comment to a bucket - void Schedule(int fmtIndex, CommentCluster cluster) - { - if (!injectBeforeFmtIndex.TryGetValue(fmtIndex, out var list)) - injectBeforeFmtIndex[fmtIndex] = list = new List(); - list.Add(cluster); - } + // Collect comments from original and schedule injection BEFORE/AFTER a formatted code token. + var injectBeforeFmtIndex = new Dictionary>(); + var injectAfterFmtIndex = new Dictionary>(); + var eofComments = new List(); + + // Helper to add a comment to a bucket + void Schedule(int fmtIndex, CommentCluster cluster) + { + if (!injectBeforeFmtIndex.TryGetValue(fmtIndex, out var list)) + injectBeforeFmtIndex[fmtIndex] = list = new List(); + list.Add(cluster); + } + + void ScheduleAfter(int fmtIndex, CommentCluster cluster) + { + if (!injectAfterFmtIndex.TryGetValue(fmtIndex, out var list)) + injectAfterFmtIndex[fmtIndex] = list = new List(); + list.Add(cluster); + } for (int i = 0; i < orig.Count; i++) { @@ -72,26 +80,37 @@ void Schedule(int fmtIndex, CommentCluster cluster) bool startsNewLine = StartsOnNewLine(orig, i); int blankLinesBefore = startsNewLine ? CountBlankLinesBefore(orig, i) : 0; - var chunk = new StringBuilder(); - chunk.Append(orig[i].Text); - int k = i + 1; - while (k < orig.Count && IsWsOrComment(orig[k])) - { - chunk.Append(orig[k].Text); - k++; - } - i = k - 1; - - // Anchor to the *next statement header* if the immediate next code token is ';' - int anchorOrig = FindAnchorAfterComments(orig, k); - - var cluster = new CommentCluster(chunk.ToString(), startsNewLine, blankLinesBefore); - - if (anchorOrig >= 0 && mapOrigToFmt.TryGetValue(anchorOrig, out int fmtIndex)) - Schedule(fmtIndex, cluster); - else - eofComments.Add(cluster); - } + var chunk = new StringBuilder(); + chunk.Append(orig[i].Text); + int k = i + 1; + while (k < orig.Count && IsWsOrComment(orig[k])) + { + chunk.Append(orig[k].Text); + k++; + } + i = k - 1; + + // Anchor to the *next statement header* if the immediate next code token is ';' + int anchorOrig = FindAnchorAfterComments(orig, k); + + var clusterText = startsNewLine ? chunk.ToString() : chunk.ToString().TrimEnd(); + var cluster = new CommentCluster(clusterText, startsNewLine, blankLinesBefore); + + if (!startsNewLine) + { + int prevCode = PrevIndex(orig, i, IsCodeToken); + if (prevCode >= 0 && mapOrigToFmt.TryGetValue(prevCode, out int prevFmtIndex)) + { + ScheduleAfter(prevFmtIndex, cluster); + continue; + } + } + + if (anchorOrig >= 0 && mapOrigToFmt.TryGetValue(anchorOrig, out int fmtIndex)) + Schedule(fmtIndex, cluster); + else + eofComments.Add(cluster); + } // Emit the formatted stream and inject comments at anchors (before the code token) var sb = new StringBuilder(); @@ -105,25 +124,25 @@ void Schedule(int fmtIndex, CommentCluster cluster) cur++; } - if (injectBeforeFmtIndex.TryGetValue(jCode, out var toInject)) - { - foreach (var c in toInject) - { - if (c.StartsNewLine) - { - // We want: at least (1 + blankLinesBefore) newlines before the comment. - int required = 1 + c.BlankLinesBefore; - int have = TrailingNewlines(sb); - for (int add = have; add < required; add++) - sb.Append(Environment.NewLine); - } - sb.Append(c.Text); - } - } - - sb.Append(fmt[jCode].Text); - cur++; - } + if (injectBeforeFmtIndex.TryGetValue(jCode, out var toInject)) + { + foreach (var c in toInject) + { + AppendWithLayout(sb, c); + } + } + + sb.Append(fmt[jCode].Text); + cur++; + + if (injectAfterFmtIndex.TryGetValue(jCode, out var toInjectAfter)) + { + foreach (var c in toInjectAfter) + { + AppendWithLayout(sb, c); + } + } + } while (cur < fmt.Count) { sb.Append(fmt[cur].Text); cur++; } @@ -148,19 +167,26 @@ private static TSqlFragment Parse(TSqlParser parser, string sql, out IList IndicesWhere(IList tokens, Func pred) - { - var list = new List(tokens.Count); - for (int i = 0; i < tokens.Count; i++) - if (pred(tokens[i])) list.Add(i); - return list; - } - - private static int NextIndex(IList tokens, int start, Func pred) - { - for (int i = start; i < tokens.Count; i++) - if (pred(tokens[i])) return i; - return -1; + private static List IndicesWhere(IList tokens, Func pred) + { + var list = new List(tokens.Count); + for (int i = 0; i < tokens.Count; i++) + if (pred(tokens[i])) list.Add(i); + return list; + } + + private static int PrevIndex(IList tokens, int start, Func pred) + { + for (int i = start; i >= 0; i--) + if (pred(tokens[i])) return i; + return -1; + } + + private static int NextIndex(IList tokens, int start, Func pred) + { + for (int i = start; i < tokens.Count; i++) + if (pred(tokens[i])) return i; + return -1; } private static bool IsCommentToken(TSqlParserToken t) => @@ -196,17 +222,38 @@ private static bool StartsOnNewLine(IList tokens, int commentIn return false; } - private static bool EndsWithNewline(StringBuilder sb) - { - if (sb.Length == 0) return false; - char last = sb[sb.Length - 1]; - return last == '\n' || last == '\r'; - } - - private static bool IsWsOrComment(TSqlParserToken t) => - t.TokenType == TSqlTokenType.WhiteSpace || - t.TokenType == TSqlTokenType.SingleLineComment || - t.TokenType == TSqlTokenType.MultilineComment; + private static bool EndsWithNewline(StringBuilder sb) + { + if (sb.Length == 0) return false; + char last = sb[sb.Length - 1]; + return last == '\n' || last == '\r'; + } + + private static bool IsWsOrComment(TSqlParserToken t) => + t.TokenType == TSqlTokenType.WhiteSpace || + t.TokenType == TSqlTokenType.SingleLineComment || + t.TokenType == TSqlTokenType.MultilineComment; + + private static bool EndsWithWhitespace(StringBuilder sb) => + sb.Length > 0 && char.IsWhiteSpace(sb[sb.Length - 1]); + + private static void AppendWithLayout(StringBuilder sb, CommentCluster cluster) + { + if (cluster.StartsNewLine) + { + // We want: at least (1 + blankLinesBefore) newlines before the comment. + int required = 1 + cluster.BlankLinesBefore; + int have = TrailingNewlines(sb); + for (int add = have; add < required; add++) + sb.Append(Environment.NewLine); + } + else if (!EndsWithWhitespace(sb)) + { + sb.Append(' '); + } + + sb.Append(cluster.Text); + } // Normalized comparison key for LCS: (TokenType, normalized text). From ab7bd13a7ace1b3abace47eb9593488e79e93094 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Sun, 16 Nov 2025 16:18:42 -0700 Subject: [PATCH 2/3] Adjust comment indentation handling --- .../TsqlFormatterCommentInterleaver.cs | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs index 174f455..3c9f65e 100644 --- a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs +++ b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs @@ -243,7 +243,7 @@ private static void AppendWithLayout(StringBuilder sb, CommentCluster cluster) { // We want: at least (1 + blankLinesBefore) newlines before the comment. int required = 1 + cluster.BlankLinesBefore; - int have = TrailingNewlines(sb); + int have = TrailingNewlinesOrLineStart(sb); for (int add = have; add < required; add++) sb.Append(Environment.NewLine); } @@ -353,18 +353,46 @@ private static int FindAnchorAfterComments(IList tokens, int st return idx; } - private static int TrailingNewlines(StringBuilder sb) - { - int c = 0; - for (int i = sb.Length - 1; i >= 0; i--) - { - char ch = sb[i]; - if (ch == '\n') c++; - else if (ch == '\r') continue; - else break; - } - return c; - } + private static int TrailingNewlinesOrLineStart(StringBuilder sb) + { + // Counts newline characters that appear after the last non-whitespace character. + // This treats trailing indentation (spaces/tabs) as being on the same line, avoiding + // inserting an extra blank line when we're already positioned at the start of a line. + int c = 0; + for (int i = sb.Length - 1; i >= 0; i--) + { + char ch = sb[i]; + if (ch == '\n') + { + c++; + } + else if (ch == '\r') + { + continue; + } + else if (ch == ' ' || ch == '\t') + { + continue; + } + else + { + break; + } + } + + // If we reached the start without finding non-whitespace, we are effectively at line start. + if (c == 0 && sb.Length > 0) + { + bool onlyWhitespace = true; + for (int i = 0; i < sb.Length; i++) + { + if (!char.IsWhiteSpace(sb[i])) { onlyWhitespace = false; break; } + } + if (onlyWhitespace) return 1; + } + + return c; + } private sealed class CommentCluster { From fd7797c3ef0c5d519cfe784109ef5dde6b0e1e03 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Sun, 16 Nov 2025 16:24:17 -0700 Subject: [PATCH 3/3] Preserve newline after inline comments --- AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs index 3c9f65e..1021338 100644 --- a/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs +++ b/AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs @@ -93,7 +93,10 @@ void ScheduleAfter(int fmtIndex, CommentCluster cluster) // Anchor to the *next statement header* if the immediate next code token is ';' int anchorOrig = FindAnchorAfterComments(orig, k); - var clusterText = startsNewLine ? chunk.ToString() : chunk.ToString().TrimEnd(); + var rawCluster = chunk.ToString(); + var clusterText = startsNewLine + ? rawCluster + : (HasNewline(rawCluster) ? rawCluster : rawCluster.TrimEnd()); var cluster = new CommentCluster(clusterText, startsNewLine, blankLinesBefore); if (!startsNewLine)