Skip to content
Merged
Show file tree
Hide file tree
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
35 changes: 35 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Third-Party Notices

This project incorporates material from the projects listed below.

---

## SqlFormatter

- **Source:** https://github.com/madskristensen/SqlFormatter
- **License:** MIT (Apache-2.0 per repository; individual files carry MIT terms)
- **Copyright:** Copyright (c) Mads Kristensen

The `SqlFormattingService` in this project was inspired by and partially derived
from the SqlFormatter extension for Visual Studio by Mads Kristensen. It uses
`Microsoft.SqlServer.TransactSql.ScriptDom` for T-SQL parsing and formatting.

### MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
20 changes: 10 additions & 10 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,39 @@
<Border Grid.Row="0" Background="{DynamicResource BackgroundDarkBrush}" Padding="8,6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Spacing="4">
<Button x:Name="PlanConnectButton" Content="Connect" Click="PlanConnect_Click"
Height="28" Padding="10,0" FontSize="12"
Height="28" Padding="8,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Connect to a SQL Server for schema lookups"/>
<TextBlock x:Name="PlanServerLabel" Text=""
VerticalAlignment="Center" FontSize="12"
Foreground="{DynamicResource ForegroundBrush}" Margin="6,0,0,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0,0,0"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="6,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<ComboBox x:Name="PlanDatabaseBox" Width="180" Height="28" FontSize="12"
IsEnabled="False" PlaceholderText="Database"
SelectionChanged="PlanDatabase_SelectionChanged"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="6,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button Content="+" Click="ZoomIn_Click" Width="28" Height="28" Padding="0" FontSize="16"
FontWeight="Bold" ToolTip.Tip="Zoom In"
Theme="{StaticResource AppButton}"/>
<Button Content="&#x2212;" Click="ZoomOut_Click" Width="28" Height="28" Padding="0" FontSize="16"
FontWeight="Bold" Margin="4,0,0,0" ToolTip.Tip="Zoom Out"
FontWeight="Bold" ToolTip.Tip="Zoom Out"
Theme="{StaticResource AppButton}"/>
<Button Content="Fit" Click="ZoomFit_Click" Height="28" Padding="8,0" Margin="4,0,0,0"
<Button Content="Fit" Click="ZoomFit_Click" Height="28" Padding="8,0"
ToolTip.Tip="Zoom to Fit"
Theme="{StaticResource AppButton}"/>
<TextBlock x:Name="ZoomLevelText" Text="100%" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="8,0,0,0" FontSize="11"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0,0,0" FontSize="11"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="12,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="6,0"/>
<Button Content="Save .sqlplan" Click="SavePlan_Click" Height="28" Padding="8,0"
ToolTip.Tip="Save plan as .sqlplan file"
Theme="{StaticResource AppButton}"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="12,0"
Foreground="{DynamicResource ForegroundBrush}" Margin="6,0"
x:Name="StatementsButtonSeparator" IsVisible="False"/>
<Button x:Name="StatementsButton" Content="Statements" Click="ToggleStatements_Click"
Height="28" Padding="8,0" IsVisible="False"
Expand Down
44 changes: 27 additions & 17 deletions src/PlanViewer.App/Controls/QuerySessionControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,77 @@
<Border Grid.Row="0" Background="{DynamicResource BackgroundDarkBrush}" Padding="8,6"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Spacing="6">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Spacing="4">
<Button x:Name="ConnectButton" Content="Connect" Click="Connect_Click"
Height="28" Padding="10,0" FontSize="12"
Height="28" Padding="8,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Connect to a SQL Server"/>
<TextBlock x:Name="ServerLabel" Text="Not connected"
VerticalAlignment="Center" FontSize="12"
Foreground="{DynamicResource ForegroundBrush}"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0,0,0"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<ComboBox x:Name="DatabaseBox" Width="200" Height="28" FontSize="12"
IsEnabled="False" PlaceholderText="Database"
SelectionChanged="Database_SelectionChanged"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="ExecuteButton" Content="&#x25B6; Actual Plan" Click="Execute_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Run query and capture execution plan with runtime stats (F5 / Ctrl+E)"/>
<Button x:Name="ExecuteEstButton" Content="&#x25C7; Estimated Plan" Click="ExecuteEstimated_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Capture estimated plan without executing (Ctrl+L)"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="HumanAdviceButton" Content="&#x1F9D1; Human Advice"
Click="HumanAdvice_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Human-readable plan analysis"/>
<Button x:Name="RobotAdviceButton" Content="&#x1F916; Robot Advice"
Click="RobotAdvice_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="JSON analysis for LLMs and automation"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="ComparePlansButton" Content="&#x2194; Compare Plans"
Click="ComparePlans_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Compare two plan tabs"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="QueryStoreButton" Content="&#x1F4CA; Query Store"
Click="QueryStore_Click"
Height="28" Padding="10,0" FontSize="12"
Height="28" Padding="8,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Analyze top queries from Query Store"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
Foreground="{DynamicResource ForegroundBrush}" Margin="2,0"/>
<Button x:Name="CopyReproButton" Content="&#x1F4CB; Copy Repro"
Click="CopyRepro_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Copy reproduction script to clipboard"/>
<Button x:Name="GetActualPlanButton" Content="&#x25B6; Run Repro"
Click="GetActualPlan_Click"
Height="28" Padding="10,0" FontSize="12" IsEnabled="False"
Height="28" Padding="8,0" FontSize="12" IsEnabled="False"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Execute the repro script and capture actual plan with runtime stats"/>
<TextBlock Text="|" VerticalAlignment="Center"
Foreground="{DynamicResource ForegroundBrush}" Margin="4,0"/>
<Button x:Name="FormatButton" Content="&#x1F4DD; Format" Click="Format_Click"
Height="28" Padding="10,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Format the SQL query"/>
<Button x:Name="FormatOptionsButton" Content="&#x2699; Format Options" Click="FormatOptions_Click"
Height="28" Padding="10,0" FontSize="12"
Theme="{StaticResource AppButton}"
ToolTip.Tip="Configure SQL formatting options"/>
</StackPanel>
<TextBlock x:Name="StatusText" DockPanel.Dock="Right"
HorizontalAlignment="Right" VerticalAlignment="Center"
Expand Down
93 changes: 92 additions & 1 deletion src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,22 @@ public QuerySessionControl(ICredentialService credentialService, ConnectionStore
QueryEditor.TextArea.TextEntered += OnTextEntered;

// Focus the editor when the control is attached to the visual tree
// Re-install TextMate if it was disposed on detach (tab switching disposes it)
AttachedToVisualTree += (_, _) =>
{
if (_textMateInstallation == null)
SetupSyntaxHighlighting();

QueryEditor.Focus();
QueryEditor.TextArea.Focus();
};

DetachedFromVisualTree += (_, _) => _textMateInstallation?.Dispose();
// Dispose TextMate when detached (e.g. tab switch) to release renderers/transformers
DetachedFromVisualTree += (_, _) =>
{
_textMateInstallation?.Dispose();
_textMateInstallation = null;
};
Comment on lines 72 to +86
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix for #234 looks right. Two notes:

  1. SetupSyntaxHighlighting() is called on every re-attach; confirm it doesn't leak the RegistryOptions / grammar objects (the old _textMateInstallation?.Dispose() only releases the installation, not whatever InstallTextMate allocated on the editor). If it's cheap, fine.
  2. Tab-switch churn now runs a full TextMate reinstall every time — worth a quick perf sanity check with a busy session that has many tabs.

Generated by Claude Code


// Focus the editor when the Editor tab is selected; toggle plan-dependent buttons
SubTabControl.SelectionChanged += (_, _) =>
Expand Down Expand Up @@ -1996,4 +2005,86 @@ private Window GetParentWindow()
var parent = this.VisualRoot;
return parent as Window ?? throw new InvalidOperationException("No parent window");
}

private async void Format_Click(object? sender, RoutedEventArgs e)
{
var sql = QueryEditor.Text;
if (string.IsNullOrWhiteSpace(sql))
return;

FormatButton.IsEnabled = false;
SetStatus("Formatting...");

try
{
var settings = SqlFormatSettingsService.Load(out var loadError);
if (loadError != null)
SetStatus("Warning: using default format settings (load failed)");

var (formatted, errors) = await Task.Run(() => SqlFormattingService.Format(sql, settings));

if (errors != null && errors.Count > 0)
{
var errorMessages = string.Join("\n", errors.Select(err => $"Line {err.Line}: {err.Message}"));
var dialog = new Window
{
Title = "SQL Format Error",
Width = 500,
Height = 250,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Icon = GetParentWindow().Icon,
Background = (IBrush)this.FindResource("BackgroundBrush")!,
Foreground = (IBrush)this.FindResource("ForegroundBrush")!,
Content = new StackPanel
{
Margin = new Avalonia.Thickness(20),
Children =
{
new TextBlock
{
Text = $"Could not format: {errors.Count} parse error(s)",
FontWeight = Avalonia.Media.FontWeight.Bold,
FontSize = 14,
Margin = new Avalonia.Thickness(0, 0, 0, 10)
},
new TextBlock
{
Text = errorMessages,
TextWrapping = TextWrapping.Wrap,
FontSize = 12
}
}
}
};
await dialog.ShowDialog(GetParentWindow());
SetStatus($"Format failed: {errors.Count} error(s)");
return;
}

var caretOffset = QueryEditor.CaretOffset;

QueryEditor.Document.BeginUpdate();
try
{
QueryEditor.Document.Replace(0, QueryEditor.Document.TextLength, formatted);
}
finally
{
QueryEditor.Document.EndUpdate();
}

QueryEditor.CaretOffset = Math.Min(caretOffset, QueryEditor.Document.TextLength);
SetStatus("Formatted");
}
finally
{
FormatButton.IsEnabled = true;
}
}

private void FormatOptions_Click(object? sender, RoutedEventArgs e)
{
var dialog = new Dialogs.FormatOptionsWindow();
dialog.ShowDialog(GetParentWindow());
}
}
Loading
Loading