Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 164 additions & 86 deletions AxialSqlTools/Modules/TsqlFormatterCommentInterleaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, List<CommentCluster>>();
var eofComments = new List<CommentCluster>();

// 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<CommentCluster>();
list.Add(cluster);
}
// Collect comments from original and schedule injection BEFORE/AFTER a formatted code token.
var injectBeforeFmtIndex = new Dictionary<int, List<CommentCluster>>();
var injectAfterFmtIndex = new Dictionary<int, List<CommentCluster>>();
var eofComments = new List<CommentCluster>();

// 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<CommentCluster>();
list.Add(cluster);
}

void ScheduleAfter(int fmtIndex, CommentCluster cluster)
{
if (!injectAfterFmtIndex.TryGetValue(fmtIndex, out var list))
injectAfterFmtIndex[fmtIndex] = list = new List<CommentCluster>();
list.Add(cluster);
}

for (int i = 0; i < orig.Count; i++)
{
Expand All @@ -72,26 +80,40 @@ 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 rawCluster = chunk.ToString();
var clusterText = startsNewLine
? rawCluster
: (HasNewline(rawCluster) ? rawCluster : rawCluster.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();
Expand All @@ -105,25 +127,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++; }

Expand All @@ -148,19 +170,26 @@ private static TSqlFragment Parse(TSqlParser parser, string sql, out IList<Parse
}
}

private static List<int> IndicesWhere(IList<TSqlParserToken> tokens, Func<TSqlParserToken, bool> pred)
{
var list = new List<int>(tokens.Count);
for (int i = 0; i < tokens.Count; i++)
if (pred(tokens[i])) list.Add(i);
return list;
}

private static int NextIndex(IList<TSqlParserToken> tokens, int start, Func<TSqlParserToken, bool> pred)
{
for (int i = start; i < tokens.Count; i++)
if (pred(tokens[i])) return i;
return -1;
private static List<int> IndicesWhere(IList<TSqlParserToken> tokens, Func<TSqlParserToken, bool> pred)
{
var list = new List<int>(tokens.Count);
for (int i = 0; i < tokens.Count; i++)
if (pred(tokens[i])) list.Add(i);
return list;
}

private static int PrevIndex(IList<TSqlParserToken> tokens, int start, Func<TSqlParserToken, bool> pred)
{
for (int i = start; i >= 0; i--)
if (pred(tokens[i])) return i;
return -1;
}

private static int NextIndex(IList<TSqlParserToken> tokens, int start, Func<TSqlParserToken, bool> pred)
{
for (int i = start; i < tokens.Count; i++)
if (pred(tokens[i])) return i;
return -1;
}

private static bool IsCommentToken(TSqlParserToken t) =>
Expand Down Expand Up @@ -196,17 +225,38 @@ private static bool StartsOnNewLine(IList<TSqlParserToken> 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 = TrailingNewlinesOrLineStart(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).
Expand Down Expand Up @@ -306,18 +356,46 @@ private static int FindAnchorAfterComments(IList<TSqlParserToken> 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
{
Expand Down