From bce2e683bbdfd74da93b11bee7264154cd41d8f6 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Sat, 22 Nov 2025 15:52:27 -0700 Subject: [PATCH 1/5] Add select alignment formatting option --- .../FormatOptionsDialog.xaml | 4 + .../FormatOptionsDialog.xaml.cs | 3 + AxialSqlTools/Modules/TsqlFormatter.cs | 86 +++++++++++-------- .../WindowSettings/SettingsWindowControl.xaml | 5 +- .../SettingsWindowControl.xaml.cs | 8 +- 5 files changed, 62 insertions(+), 44 deletions(-) diff --git a/AxialSqlTools/Commands/FormatQueryDialog/FormatOptionsDialog.xaml b/AxialSqlTools/Commands/FormatQueryDialog/FormatOptionsDialog.xaml index bba808e..6cbdea9 100644 --- a/AxialSqlTools/Commands/FormatQueryDialog/FormatOptionsDialog.xaml +++ b/AxialSqlTools/Commands/FormatQueryDialog/FormatOptionsDialog.xaml @@ -69,6 +69,10 @@ Content="Break sproc definition parameters per line" Checked="formatSetting_Checked" Unchecked="formatSetting_Unchecked"/> + SprocDefinitionsCreate = new List(); public List SprocDefinitionsAlter = new List(); public List SprocDefinitionsCreateAlter = new List(); - public List SelectsWithTop = new List(); + public List SelectsWithTopOrDistinct = new List(); public override void ExplicitVisit(QualifiedJoin node) { @@ -197,8 +197,8 @@ public override void ExplicitVisit(DeclareVariableStatement node) public override void ExplicitVisit(QuerySpecification node) { base.ExplicitVisit(node); - if (node.TopRowFilter != null) - SelectsWithTop.Add(node); + if (node.TopRowFilter != null || node.UniqueRowFilter == UniqueRowFilter.Distinct) + SelectsWithTopOrDistinct.Add(node); } } @@ -836,43 +836,55 @@ void SplitParams(IList parameters) } - // special case #11 - split SELECT fields after TOP, fixed indent up to FROM - // TODO - can't get this right, so disabled for now + // special case #11 - split SELECT fields after DISTINCT/TOP, fixed indent up to FROM if (formatSettings.breakSelectFieldsAfterTopAndUnindent) { - //var tokens = sqlFragment.ScriptTokenStream; - //const string indent = "\t"; + var tokens = sqlFragment.ScriptTokenStream; + const int indentSpacesBetweenSelectAndFields = 7; - //foreach (var qs in visitor.SelectsWithTop) - //{ - // // 1) break right after TOP(...) into "\r\n\t" - // int splitIdx = qs.TopRowFilter.LastTokenIndex + 1; - // if (splitIdx < tokens.Count - // && tokens[splitIdx].TokenType == TSqlTokenType.WhiteSpace) - // { - // tokens[splitIdx].Text = "\r\n" + indent; - // } - - // //// 2) for every select‐element after the first, break before it into "\r\n\t" - // //for (int i = 1; i < qs.SelectElements.Count; i++) - // //{ - // // var elem = qs.SelectElements[i]; - // // int wsIdx = elem.FirstTokenIndex - 1; - // // if (wsIdx >= 0 - // // && tokens[wsIdx].TokenType == TSqlTokenType.WhiteSpace) - // // { - // // tokens[wsIdx].Text = "\r\n" + indent; - // // } - // //} - - // //// 3) unindent FROM back to column 1 - // //int wsBeforeFrom = qs.FromClause.FirstTokenIndex - 1; - // //if (wsBeforeFrom >= 0 - // // && tokens[wsBeforeFrom].TokenType == TSqlTokenType.WhiteSpace) - // //{ - // // tokens[wsBeforeFrom].Text = "\r\n"; - // //} - //} + foreach (var qs in visitor.SelectsWithTopOrDistinct) + { + if (qs.SelectElements == null || qs.SelectElements.Count == 0) + continue; + + string baseIndent = ""; + int wsBeforeSelect = qs.FirstTokenIndex - 1; + if (wsBeforeSelect >= 0 && tokens[wsBeforeSelect].TokenType == TSqlTokenType.WhiteSpace) + { + var ws = tokens[wsBeforeSelect].Text; + int lastNl = ws.LastIndexOf("\r\n"); + baseIndent = lastNl >= 0 + ? ws.Substring(lastNl + 2) + : ws; + } + + string fieldIndent = baseIndent + new string(' ', indentSpacesBetweenSelectAndFields); + + void ReplaceWhitespaceWithIndent(int idx, string indent) + { + if (idx >= 0 + && idx < tokens.Count + && tokens[idx].TokenType == TSqlTokenType.WhiteSpace) + { + tokens[idx].Text = "\r\n" + indent; + } + } + + int firstFieldWs = qs.SelectElements[0].FirstTokenIndex - 1; + ReplaceWhitespaceWithIndent(firstFieldWs, fieldIndent); + + for (int i = 1; i < qs.SelectElements.Count; i++) + { + int wsIdx = qs.SelectElements[i].FirstTokenIndex - 1; + ReplaceWhitespaceWithIndent(wsIdx, fieldIndent); + } + + if (qs.FromClause != null) + { + int wsBeforeFrom = qs.FromClause.FirstTokenIndex - 1; + ReplaceWhitespaceWithIndent(wsBeforeFrom, baseIndent); + } + } } // return full recompiled result diff --git a/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml b/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml index cce5183..4e79109 100644 --- a/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml +++ b/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml @@ -363,9 +363,8 @@ Content="Break sproc definition parameters per line" Checked="formatSetting_Checked" Unchecked="formatSetting_Unchecked"/> - - diff --git a/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml.cs b/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml.cs index 89a965e..00309e5 100644 --- a/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml.cs +++ b/AxialSqlTools/WindowSettings/SettingsWindowControl.xaml.cs @@ -102,9 +102,9 @@ private void LoadSavedSettings() BreakSprocParametersPerLine.IsChecked = tsqlCodeFormatSettings.breakSprocParametersPerLine; UppercaseBuiltInFunctions.IsChecked = tsqlCodeFormatSettings.uppercaseBuiltInFunctions; UnindentBeginEndBlocks.IsChecked = tsqlCodeFormatSettings.unindentBeginEndBlocks; - BreakVariableDefinitionsPerLine.IsChecked = tsqlCodeFormatSettings.breakVariableDefinitionsPerLine; + BreakVariableDefinitionsPerLine.IsChecked = tsqlCodeFormatSettings.breakVariableDefinitionsPerLine; BreakSprocDefinitionParametersPerLine.IsChecked = tsqlCodeFormatSettings.breakSprocDefinitionParametersPerLine; - // BreakSelectFieldsAfterTopAndUnindent.IsChecked = tsqlCodeFormatSettings.breakSelectFieldsAfterTopAndUnindent; + BreakSelectFieldsAfterTopAndUnindent.IsChecked = tsqlCodeFormatSettings.breakSelectFieldsAfterTopAndUnindent; OpenAiApiKey.Password = SettingsManager.GetOpenAiApiKey(); @@ -327,7 +327,7 @@ private void Button_SaveApplyAdditionalFormat_Click(object sender, RoutedEventAr unindentBeginEndBlocks = UnindentBeginEndBlocks.IsChecked.GetValueOrDefault(false), breakVariableDefinitionsPerLine = BreakVariableDefinitionsPerLine.IsChecked.GetValueOrDefault(false), breakSprocDefinitionParametersPerLine = BreakSprocDefinitionParametersPerLine.IsChecked.GetValueOrDefault(false), - // breakSelectFieldsAfterTopAndUnindent = BreakSelectFieldsAfterTopAndUnindent.IsChecked.GetValueOrDefault(false) + breakSelectFieldsAfterTopAndUnindent = BreakSelectFieldsAfterTopAndUnindent.IsChecked.GetValueOrDefault(false) }; SettingsManager.SaveTSqlCodeFormatSettings(settings); @@ -515,7 +515,7 @@ private void formatTSqlExample() unindentBeginEndBlocks = UnindentBeginEndBlocks.IsChecked.GetValueOrDefault(false), breakVariableDefinitionsPerLine = BreakVariableDefinitionsPerLine.IsChecked.GetValueOrDefault(false), breakSprocDefinitionParametersPerLine = BreakSprocDefinitionParametersPerLine.IsChecked.GetValueOrDefault(false), - // breakSelectFieldsAfterTopAndUnindent = BreakSelectFieldsAfterTopAndUnindent.IsChecked.GetValueOrDefault(false) + breakSelectFieldsAfterTopAndUnindent = BreakSelectFieldsAfterTopAndUnindent.IsChecked.GetValueOrDefault(false) }; FormattedQueryPreview.Text = TSqlFormatter.FormatCode(SourceQueryPreview.Text, settings); From 1208dca880dbf1f71aa4a655f809d09ab2a92f20 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:30:41 -0700 Subject: [PATCH 2/5] Adjust select field indentation after DISTINCT/TOP --- AxialSqlTools/Modules/TsqlFormatter.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AxialSqlTools/Modules/TsqlFormatter.cs b/AxialSqlTools/Modules/TsqlFormatter.cs index dd8159b..0c69462 100644 --- a/AxialSqlTools/Modules/TsqlFormatter.cs +++ b/AxialSqlTools/Modules/TsqlFormatter.cs @@ -840,7 +840,9 @@ void SplitParams(IList parameters) if (formatSettings.breakSelectFieldsAfterTopAndUnindent) { var tokens = sqlFragment.ScriptTokenStream; - const int indentSpacesBetweenSelectAndFields = 7; + // Keep field indentation predictable: always one indent level past the SELECT line. + // Prefer tabs when the surrounding block already uses tabs; otherwise fall back to 4 spaces. + string indentAfterSelect = "\t"; foreach (var qs in visitor.SelectsWithTopOrDistinct) { @@ -858,7 +860,8 @@ void SplitParams(IList parameters) : ws; } - string fieldIndent = baseIndent + new string(' ', indentSpacesBetweenSelectAndFields); + string indentUnit = baseIndent.Contains("\t") ? indentAfterSelect : new string(' ', 4); + string fieldIndent = baseIndent + indentUnit; void ReplaceWhitespaceWithIndent(int idx, string indent) { From 27a639e21502fc12016ed25026050fa61772233c Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:35:19 -0700 Subject: [PATCH 3/5] Refine DISTINCT/TOP select indentation --- AxialSqlTools/Modules/TsqlFormatter.cs | 43 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/AxialSqlTools/Modules/TsqlFormatter.cs b/AxialSqlTools/Modules/TsqlFormatter.cs index 0c69462..1d7ab89 100644 --- a/AxialSqlTools/Modules/TsqlFormatter.cs +++ b/AxialSqlTools/Modules/TsqlFormatter.cs @@ -844,6 +844,31 @@ void SplitParams(IList parameters) // Prefer tabs when the surrounding block already uses tabs; otherwise fall back to 4 spaces. string indentAfterSelect = "\t"; + string ExtractLineIndent(string whitespace) + { + int lastNl = whitespace.LastIndexOf("\r\n"); + return lastNl >= 0 ? whitespace.Substring(lastNl + 2) : whitespace; + } + + void ReduceInnerIndent(int startIdx, int endIdx, string indentUnit, int times) + { + if (times <= 0 || string.IsNullOrEmpty(indentUnit)) + return; + + for (int i = startIdx; i <= endIdx && i < tokens.Count; i++) + { + if (tokens[i].TokenType != TSqlTokenType.WhiteSpace) + continue; + + string ws = tokens[i].Text; + for (int n = 0; n < times; n++) + { + ws = RemoveOneIndent(ws, indentUnit); + } + tokens[i].Text = ws; + } + } + foreach (var qs in visitor.SelectsWithTopOrDistinct) { if (qs.SelectElements == null || qs.SelectElements.Count == 0) @@ -853,11 +878,7 @@ void SplitParams(IList parameters) int wsBeforeSelect = qs.FirstTokenIndex - 1; if (wsBeforeSelect >= 0 && tokens[wsBeforeSelect].TokenType == TSqlTokenType.WhiteSpace) { - var ws = tokens[wsBeforeSelect].Text; - int lastNl = ws.LastIndexOf("\r\n"); - baseIndent = lastNl >= 0 - ? ws.Substring(lastNl + 2) - : ws; + baseIndent = ExtractLineIndent(tokens[wsBeforeSelect].Text); } string indentUnit = baseIndent.Contains("\t") ? indentAfterSelect : new string(' ', 4); @@ -869,7 +890,19 @@ void ReplaceWhitespaceWithIndent(int idx, string indent) && idx < tokens.Count && tokens[idx].TokenType == TSqlTokenType.WhiteSpace) { + string original = tokens[idx].Text; + string originalIndent = ExtractLineIndent(original); tokens[idx].Text = "\r\n" + indent; + + // If we are pulling the element to the left, keep nested structure + // aligned by reducing inner whitespace by the same indent units. + if (originalIndent.Length > indent.Length) + { + int diff = originalIndent.Length - indent.Length; + int unitSize = Math.Max(1, indentUnit.Length); + int times = diff / unitSize; + ReduceInnerIndent(idx + 1, qs.FromClause?.FirstTokenIndex - 1 ?? qs.LastTokenIndex, indentUnit, times); + } } } From 2c9e07b2c4af5ce166a56f8ef42fa200833e9650 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:39:53 -0700 Subject: [PATCH 4/5] Normalize select field spacing --- AxialSqlTools/Modules/TsqlFormatter.cs | 38 +++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/AxialSqlTools/Modules/TsqlFormatter.cs b/AxialSqlTools/Modules/TsqlFormatter.cs index 1d7ab89..f3657c0 100644 --- a/AxialSqlTools/Modules/TsqlFormatter.cs +++ b/AxialSqlTools/Modules/TsqlFormatter.cs @@ -869,11 +869,47 @@ void ReduceInnerIndent(int startIdx, int endIdx, string indentUnit, int times) } } + string SingleLineIndent(string indent) + { + // collapse any accidental blank lines and keep a single newline + indent + return "\r\n" + (indent ?? string.Empty); + } + + string CollapseBlankLines(string ws) + { + if (string.IsNullOrEmpty(ws)) + return ws; + + while (ws.Contains("\r\n\r\n")) + { + ws = ws.Replace("\r\n\r\n", "\r\n"); + } + return ws; + } + + void CollapseRangeWhitespace(int startIdx, int endIdx) + { + for (int i = startIdx; i <= endIdx && i < tokens.Count; i++) + { + if (i < 0) + continue; + + if (tokens[i].TokenType == TSqlTokenType.WhiteSpace) + { + tokens[i].Text = CollapseBlankLines(tokens[i].Text); + } + } + } + foreach (var qs in visitor.SelectsWithTopOrDistinct) { if (qs.SelectElements == null || qs.SelectElements.Count == 0) continue; + int normalizeStart = Math.Max(0, qs.SelectElements[0].FirstTokenIndex - 1); + int normalizeEnd = qs.FromClause?.FirstTokenIndex - 1 ?? qs.LastTokenIndex; + CollapseRangeWhitespace(normalizeStart, normalizeEnd); + string baseIndent = ""; int wsBeforeSelect = qs.FirstTokenIndex - 1; if (wsBeforeSelect >= 0 && tokens[wsBeforeSelect].TokenType == TSqlTokenType.WhiteSpace) @@ -892,7 +928,7 @@ void ReplaceWhitespaceWithIndent(int idx, string indent) { string original = tokens[idx].Text; string originalIndent = ExtractLineIndent(original); - tokens[idx].Text = "\r\n" + indent; + tokens[idx].Text = SingleLineIndent(indent); // If we are pulling the element to the left, keep nested structure // aligned by reducing inner whitespace by the same indent units. From c530bbf3fc74560c1b4a6cd44028bb894fe66ca6 Mon Sep 17 00:00:00 2001 From: Alex Bochkov Date: Mon, 24 Nov 2025 13:43:24 -0700 Subject: [PATCH 5/5] Improve SELECT alignment whitespace normalization --- AxialSqlTools/Modules/TsqlFormatter.cs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/AxialSqlTools/Modules/TsqlFormatter.cs b/AxialSqlTools/Modules/TsqlFormatter.cs index f3657c0..d0b2cbf 100644 --- a/AxialSqlTools/Modules/TsqlFormatter.cs +++ b/AxialSqlTools/Modules/TsqlFormatter.cs @@ -880,11 +880,30 @@ string CollapseBlankLines(string ws) if (string.IsNullOrEmpty(ws)) return ws; - while (ws.Contains("\r\n\r\n")) + var lines = ws.Split(new[] { "\r\n" }, StringSplitOptions.None); + List compact = new List(); + bool previousBlank = false; + + foreach (var line in lines) { - ws = ws.Replace("\r\n\r\n", "\r\n"); + bool isBlank = string.IsNullOrWhiteSpace(line); + + if (isBlank) + { + if (previousBlank) + continue; + + compact.Add(string.Empty); + } + else + { + compact.Add(line); + } + + previousBlank = isBlank; } - return ws; + + return string.Join("\r\n", compact); } void CollapseRangeWhitespace(int startIdx, int endIdx)