Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5b38cf2
Add 1.26 screenshots
erikdarlingdata Sep 16, 2026
a93f835
Merge pull request #534 from erikdarlingdata/docs/1.26-screenshots
erikdarlingdata Sep 16, 2026
0b04e8c
Refresh the README screenshots for 1.26
erikdarlingdata Sep 16, 2026
3d0ffd8
Merge pull request #535 from erikdarlingdata/docs/refresh-screenshots…
erikdarlingdata Sep 16, 2026
dcb4978
Make the CI check report on every PR
erikdarlingdata Sep 16, 2026
e2a3a58
Merge pull request #536 from erikdarlingdata/ci/always-report-build-c…
erikdarlingdata Sep 16, 2026
5483ee3
ci: bump signpath/github-action-submit-signing-request from 2 to 3
dependabot[bot] Sep 21, 2026
7434220
deps: Bump the patch-and-minor group with 9 updates
dependabot[bot] Sep 21, 2026
b003864
Show the editor as soon as a session connects
erikdarlingdata Sep 21, 2026
0f9b11f
Merge pull request #541 from erikdarlingdata/fix/540-editor-after-con…
erikdarlingdata Sep 21, 2026
09c539c
Fill the database picker from the connection that just opened
erikdarlingdata Sep 21, 2026
e94694b
Merge pull request #542 from erikdarlingdata/fix/connect-flip-honest-…
erikdarlingdata Sep 21, 2026
99db12f
Give the advice header's context facts their labels back
erikdarlingdata Sep 21, 2026
a89d179
Merge pull request #543 from erikdarlingdata/fix/540-advice-context-f…
erikdarlingdata Sep 21, 2026
e0896cb
Merge pull request #538 from erikdarlingdata/dependabot/github_action…
erikdarlingdata Sep 21, 2026
4640b74
Merge pull request #539 from erikdarlingdata/dependabot/nuget/src/Pla…
erikdarlingdata Sep 21, 2026
7e6cb1d
Bump Avalonia and its coupled third-party libraries to 12
erikdarlingdata Sep 21, 2026
f628de2
Follow Avalonia 12's renames and retire the comments it falsified
erikdarlingdata Sep 21, 2026
4c32608
Keep the harness honest about what Avalonia 12 changed under it
erikdarlingdata Sep 21, 2026
27307d4
Re-measure the toolbar thresholds the new text metrics moved
erikdarlingdata Sep 21, 2026
68079b2
Say what the publish actually does with libSkiaSharp.so
erikdarlingdata Sep 21, 2026
61d45c5
Do not claim a real-metrics width this suite never measured
erikdarlingdata Sep 21, 2026
b18a53b
State the measured line heights as what they are
erikdarlingdata Sep 21, 2026
939ba9e
Find the session's window the way Avalonia 12 requires
erikdarlingdata Sep 21, 2026
f43205e
Say which toolbar commands may never collapse, and nail the height fa…
erikdarlingdata Sep 21, 2026
718022b
Survive Avalonia 12's headless session-setup race without retrying tests
erikdarlingdata Sep 21, 2026
095c248
Name the race's issue and balance the metrics paragraphs
erikdarlingdata Sep 21, 2026
d636c8d
Dispose the schema viewers' TextMate installations
erikdarlingdata Sep 22, 2026
87d7382
Merge pull request #550 from erikdarlingdata/fix/546-schema-textmate-…
erikdarlingdata Sep 22, 2026
12f64dc
Keep loading bars animating across a tab switch
erikdarlingdata Sep 22, 2026
9923d26
Merge pull request #549 from erikdarlingdata/upgrade/avalonia-12-take2
erikdarlingdata Sep 22, 2026
e4003f3
Drop the unused AvaloniaEdit.TextMate.Grammars package
erikdarlingdata Sep 22, 2026
dee070b
Let the escape-brackets label keep its underscore
erikdarlingdata Sep 22, 2026
45706cd
Pin the scrollbar contract on a grid whose bars actually realize
erikdarlingdata Sep 22, 2026
399b420
Merge pull request #552 from erikdarlingdata/fix/545-drop-grammars-pa…
erikdarlingdata Sep 22, 2026
2af84be
Merge pull request #553 from erikdarlingdata/fix/547-escape-brackets-…
erikdarlingdata Sep 22, 2026
182eb77
Merge pull request #554 from erikdarlingdata/fix/548-datagrid-columns…
erikdarlingdata Sep 22, 2026
b07a2c6
Let typing follow Ctrl+N without a click
erikdarlingdata Sep 22, 2026
b2b0907
Merge pull request #555 from erikdarlingdata/fix/551-focus-after-new-tab
erikdarlingdata Sep 22, 2026
6314f2c
Read only the function's own comparison in the non-SARGable check
erikdarlingdata Sep 23, 2026
e7c7e71
Merge pull request #557 from erikdarlingdata/fix/556-compound-predica…
erikdarlingdata Sep 23, 2026
e3fe946
Warn about a join OR only when a lookup reads another input
erikdarlingdata Sep 24, 2026
a2f2d92
Merge pull request #559 from erikdarlingdata/fix/558-join-or-paramete…
erikdarlingdata Sep 24, 2026
9488bf0
Leave the sp_executesql declaration list out of substituted text
erikdarlingdata Sep 24, 2026
381f0e0
Merge pull request #560 from erikdarlingdata/fix/sp-executesql-declar…
erikdarlingdata Sep 24, 2026
0ba9135
Require 1,000ms statement elapsed before rule 35 fires
erikdarlingdata Sep 24, 2026
1016100
Detect a non-SARGable function or conversion on an unaliased table-va…
erikdarlingdata Sep 24, 2026
83c3c33
Correct the table-variable history in the column-reference comments
erikdarlingdata Sep 24, 2026
e119f8f
Merge pull request #563 from erikdarlingdata/fix/562-expensive-operat…
erikdarlingdata Sep 24, 2026
d4062ed
Merge pull request #565 from erikdarlingdata/fix/561-table-variable-b…
erikdarlingdata Sep 24, 2026
9a04629
Skip an outer reference when checking a scan predicate for non-SARGab…
erikdarlingdata Sep 24, 2026
1374b75
Clean a temp table's full tempdb name before comparing owners
erikdarlingdata Sep 24, 2026
24f6c61
Merge pull request #566 from erikdarlingdata/fix/564-outer-reference-…
erikdarlingdata Sep 24, 2026
fb0c0e9
Bump version to 1.27.0 (#567)
erikdarlingdata Sep 24, 2026
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
46 changes: 33 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,13 @@ name: CI
on:
push:
branches: [main]
# No paths-ignore here, deliberately. A required status check has to REPORT on every
# PR, and a workflow skipped by a path filter reports nothing at all -- GitHub then
# shows the check as forever pending and the PR can never merge. A docs-only PR would
# deadlock. The filtering moved into the job below, where the steps are skipped but the
# job still finishes and reports success. Same shape PerformanceMonitor's build.yml uses.
pull_request:
branches: [main, dev]
paths-ignore:
- '**.md'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- 'CITATION.cff'
- 'llms.txt'
- '.github/ISSUE_TEMPLATE/**'
- 'docs/**'
- 'screenshots/**'
- 'server/**'
- 'src/PlanViewer.Ssms/**'
- 'src/PlanViewer.Ssms.Installer/**'

permissions:
contents: read
Expand All @@ -29,21 +21,49 @@ jobs:
steps:
- uses: actions/checkout@v7

# What used to be the workflow's paths-ignore list. Anything NOT matched here is
# code, and only then is there anything to build.
- name: Classify changed paths
uses: dorny/paths-filter@v4
id: filter
with:
# Positive list, not negations. paths-filter ORs the patterns in a filter, so a
# stack of '!' patterns matches whenever a file fails ANY one of them, which for
# a docs-only change is always true. Listing what IS code keeps the OR honest.
# PlanViewer.Ssms and PlanViewer.Ssms.Installer stay out: they are not in the
# solution and ci.yml never built them.
filters: |
code:
- 'src/PlanViewer.App/**'
- 'src/PlanViewer.Cli/**'
- 'src/PlanViewer.Core/**'
- 'src/PlanViewer.Web/**'
- 'src/Directory.Build.props'
- 'tests/**'
- 'PlanViewer.sln'
- 'global.json'
- '.github/workflows/ci.yml'

- name: Setup .NET 10.0
if: steps.filter.outputs.code == 'true'
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
cache: true
cache-dependency-path: '**/*.csproj'

- name: Install WASM workload
if: steps.filter.outputs.code == 'true'
run: dotnet workload install wasm-tools

- name: Restore solution
if: steps.filter.outputs.code == 'true'
run: dotnet restore PlanViewer.sln

- name: Build solution
if: steps.filter.outputs.code == 'true'
run: dotnet build PlanViewer.sln -c Release --no-restore

- name: Run tests
if: steps.filter.outputs.code == 'true'
run: dotnet test tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-build --verbosity normal -- --hangdump --hangdump-timeout 5m --hangdump-type none
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ jobs:
path: publish/win-x64/

- name: Sign Windows build
uses: signpath/github-action-submit-signing-request@v2
uses: signpath/github-action-submit-signing-request@v3
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '7969f8b6-d946-4a74-9bac-a55856d8b8e0'
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ authors:
website: "https://erikdarling.com"
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
license: MIT
version: "1.26.0"
date-released: "2026-09-16"
version: "1.27.0"
date-released: "2026-09-24"
keywords:
- sql-server
- execution-plan
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ Navigate stored procedures and batches with multiple statements. Click any state
![Navigate Stored Procedure Statements and Plans](screenshots/Navigate%20Stored%20Procedure%20Statements%20and%20Plans.png)

### Operator Tooltip and Properties
Hover over any operator for a detailed tooltip with costs, rows, I/O, timing, parallelism, and warnings. Click to open the full properties panel with per-thread timing, predicates, and more.
Hover any operator for a grouped tooltip with costs, rows, timing, and parallelism. Click for the full properties panel, which has a filter box and folds per-thread stats into one expander per section instead of hundreds of rows.

![Operator Tooltip](screenshots/Actual%20Execution%20Plan%20With%20Warning%20Tool%20Tip.png)

![Operator Properties](screenshots/Operator%20Properties.png)

### Advice for Humans
One-click text report with server context, warnings, wait stats, and expensive operators — ready to read or share.
Severity-scored cards for each statement: the warnings that fired, wait stats, memory grant, and missing indexes. Copy to clipboard gives you the plain-text version to paste into a ticket.

![Advice for Humans](screenshots/Advice%20For%20Humans.png)

### Plan Comparison
Side-by-side comparison of two plans showing cost, runtime, I/O, memory, and wait stat differences.
A real metric diff. Each statement is scored as regressed or improved, every metric carries its own delta chip, and the direction that counts as better is declared per metric rather than guessed from the sign.

![Plan Comparison](screenshots/Plan%20Comparison.png)

Expand Down
Binary file modified screenshots/Actual Execution Plan With Warning Tool Tip.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Actual Execution Plan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Advice For Humans.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Navigate Stored Procedure Statements and Plans.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Operator Properties.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Plan Comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/Query Editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Tests and server/ projects are outside src/ and are unaffected.
-->
<PropertyGroup>
<Version>1.26.0</Version>
<Version>1.27.0</Version>
<Authors>Erik Darling</Authors>
<Company>Darling Data LLC</Company>
<Product>Performance Studio</Product>
Expand Down
10 changes: 5 additions & 5 deletions src/PlanViewer.App/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
<Setter Property="AllowAutoHide" Value="True"/>
</Style>

<!-- DataGrid does not scroll through a ScrollViewer, and the ScrollBar rule above
cannot reach its bars either: it assigns AllowAutoHide to them in code, and a
local value outranks any style. There is no DataGrid.AllowAutoHide to set, but
the value it pushes down is the ATTACHED ScrollViewer property read off itself,
so setting that here is what reaches the grids. -->
<!-- DataGrid does not scroll through a ScrollViewer, so the ScrollBar rule above
cannot reach its bars: PART_HorizontalScrollbar and PART_VerticalScrollbar are
bare ScrollBars inside its own template. There is no DataGrid.AllowAutoHide to
set, but the template binds each bar's AllowAutoHide to the ATTACHED ScrollViewer
property on the grid, so setting that here is what reaches them. -->
<Style Selector="DataGrid">
<Setter Property="ScrollViewer.AllowAutoHide" Value="True"/>
</Style>
Expand Down
22 changes: 18 additions & 4 deletions src/PlanViewer.App/Controls/PlanViewerControl.Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,24 @@ private void ShowSchemaResult(string title, string content)
Padding = new Thickness(4)
};

// SQL syntax highlighting
var registryOptions = new TextMateSharp.Grammars.RegistryOptions(TextMateSharp.Grammars.ThemeName.DarkPlus);
var tm = editor.InstallTextMate(registryOptions);
tm.SetGrammar(registryOptions.GetScopeByLanguageId("sql"));
/* SQL syntax highlighting, on the query editor's own lifecycle: installed on attach,
disposed on detach (#546). An installation owns a tokenization model with its own
thread, and a thread roots itself against GC — so installing once and never disposing
leaked a live thread for every schema tab ever closed. Detach also fires on plain tab
switches, which is why attach re-installs. */
TextMate.Installation? tm = null;
editor.AttachedToVisualTree += (_, _) =>
{
if (tm != null) return;
var registryOptions = new TextMateSharp.Grammars.RegistryOptions(TextMateSharp.Grammars.ThemeName.DarkPlus);
tm = editor.InstallTextMate(registryOptions);
tm.SetGrammar(registryOptions.GetScopeByLanguageId("sql"));
};
editor.DetachedFromVisualTree += (_, _) =>
{
tm?.Dispose();
tm = null;
};

// Context menu
var copyItem = new MenuItem { Header = "Copy" };
Expand Down
2 changes: 1 addition & 1 deletion src/PlanViewer.App/Controls/PlanViewerControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@
Background="{DynamicResource BackgroundBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<TextBox x:Name="PropertiesFilterBox"
Watermark="Filter properties..."
PlaceholderText="Filter properties..."
FontSize="11" Height="24" MinHeight="0" Padding="6,1"
VerticalContentAlignment="Center"
TextChanged="PropertiesFilter_TextChanged"
Expand Down
78 changes: 48 additions & 30 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Avalonia.Controls.Templates;
using Avalonia.Platform.Storage;
using AvaloniaEdit.TextMate;
using Microsoft.Data.SqlClient;
using PlanViewer.App.Dialogs;
using PlanViewer.Core.Interfaces;
using PlanViewer.App.Helpers;
Expand Down Expand Up @@ -329,7 +328,10 @@ public void SetConnectionServices(ICredentialService credentialService, Connecti
}

/// <summary>
/// Update the connection UI to reflect an active connection (used when connection is inherited).
/// Update the connection UI to reflect an active connection (used when connection is
/// inherited). Label and button only — the session-hosted viewers that call this hide the
/// whole connection toolbar, so there is no picker to feed. A standalone tab with a live
/// toolbar wants <see cref="AdoptConnection"/> instead.
/// </summary>
public void SetConnectionStatus(string serverName, string? database)
{
Expand All @@ -340,6 +342,27 @@ public void SetConnectionStatus(string serverName, string? database)
_planSelectedDatabase = database;
}

/// <summary>
/// Takes over a connection the ConnectionDialog just validated, for a standalone tab whose
/// toolbar is visible: paints the status AND fills, enables and pre-selects the database
/// picker, with <see cref="_planConnection"/> set so changing the picker actually switches
/// <see cref="ConnectionString"/>. Status alone left a green label over a disabled, empty
/// picker — the same lie the connect handlers used to tell (#540 follow-up).
///
/// <para>Call <see cref="SetConnectionServices"/> first: the picker's SelectionChanged
/// rebuilds the connection string through the credential service.</para>
/// </summary>
public void AdoptConnection(ServerConnection connection, string? database,
IReadOnlyList<string> databases)
{
_planConnection = connection;
SetConnectionStatus(connection.ServerName, database);

PlanDatabaseBox.ItemsSource = databases;
PlanDatabaseBox.IsEnabled = true;
SelectPlanDatabase();
}

// Events for MainWindow to wire up advice/repro actions
public event EventHandler? HumanAdviceRequested;
public event EventHandler? RobotAdviceRequested;
Expand Down Expand Up @@ -591,39 +614,34 @@ private async void PlanConnect_Click(object? sender, RoutedEventArgs e)
PlanServerLabel.Foreground = FindBrushResource("SuccessBrush");
PlanConnectButton.Content = AppIcons.MakeContent(AppIcons.Connect, "Reconnect");

// Populate database dropdown
try
{
var connStr = _planConnection.GetConnectionString(_planCredentialService, "master");
await using var conn = new SqlConnection(connStr);
await conn.OpenAsync();

var databases = new List<string>();
using var cmd = new SqlCommand(
"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' ORDER BY name", conn);
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
databases.Add(reader.GetString(0));
/* The dialog only closes with true after it opened this connection and enumerated
these databases — through the database the user named, which is the one some
logins (Azure SQL DB, JIT access) can open when master is off limits. Asking
again here through a second, hardcoded-master connection was a wasted round trip
whose swallowed failure left a green toolbar over a dead database picker. Same
hand-over QuerySessionControl's connect block takes. */
PlanDatabaseBox.ItemsSource = dialog.ResultDatabases;
PlanDatabaseBox.IsEnabled = true;
SelectPlanDatabase();
}

PlanDatabaseBox.ItemsSource = databases;
PlanDatabaseBox.IsEnabled = true;
/// <summary>
/// Points the picker at <see cref="_planSelectedDatabase"/> when the list holds it. The
/// selection this raises recomputes the same ConnectionString the caller already set, which
/// is idempotent on purpose — the handler is the one place the string is derived.
/// </summary>
private void SelectPlanDatabase()
{
if (_planSelectedDatabase == null) return;

if (_planSelectedDatabase != null)
for (int i = 0; i < PlanDatabaseBox.Items.Count; i++)
{
if (PlanDatabaseBox.Items[i]?.ToString() == _planSelectedDatabase)
{
for (int i = 0; i < PlanDatabaseBox.Items.Count; i++)
{
if (PlanDatabaseBox.Items[i]?.ToString() == _planSelectedDatabase)
{
PlanDatabaseBox.SelectedIndex = i;
break;
}
}
PlanDatabaseBox.SelectedIndex = i;
break;
}
}
catch
{
PlanDatabaseBox.IsEnabled = false;
}
}

private void PlanDatabase_SelectionChanged(object? sender, SelectionChangedEventArgs e)
Expand Down
43 changes: 16 additions & 27 deletions src/PlanViewer.App/Controls/QuerySessionControl.Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,22 @@ the rest of it has to live. */
ServerLabel.Foreground = Token("SuccessBrush", Brushes.LimeGreen);
ConnectButton.Content = Helpers.AppIcons.MakeContent(Helpers.AppIcons.Connect, "Reconnect");

await PopulateDatabases();
/* Connecting is the one way a fresh session stops being empty without a keystroke
or a document, so neither of the overlay's other triggers will fire — without
this, the "Get started" panel keeps covering the editor and offering "Connect to
a server" on a session that just did (#540). Before the awaits below, so the
editor appears the moment the dialog closes rather than after three round trips
to the server. */
RefreshEmptyState();

/* The dialog only closes with true after it opened this connection and enumerated
these databases — through the database the user named, which is the one some
logins (Azure SQL DB, JIT access) can open when master is off limits. Asking
again here through a second, hardcoded-master connection was a wasted round trip
whose swallowed failure left a green toolbar over a dead database picker. */
DatabaseBox.ItemsSource = dialog.ResultDatabases;
DatabaseBox.IsEnabled = true;

await FetchServerMetadataAsync();
await FetchServerUtcOffset();

Expand Down Expand Up @@ -86,32 +101,6 @@ the rest of it has to live. */
}
}

private async Task PopulateDatabases()
{
if (_serverConnection == null) return;

try
{
var connStr = _serverConnection.GetConnectionString(_credentialService, "master");
await using var conn = new SqlConnection(connStr);
await conn.OpenAsync();

var databases = new List<string>();
using var cmd = new SqlCommand(
"SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' ORDER BY name", conn);
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
databases.Add(reader.GetString(0));

DatabaseBox.ItemsSource = databases;
DatabaseBox.IsEnabled = true;
}
catch
{
DatabaseBox.IsEnabled = false;
}
}

private async void Database_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_serverConnection == null || DatabaseBox.SelectedItem == null) return;
Expand Down
18 changes: 11 additions & 7 deletions src/PlanViewer.App/Controls/QuerySessionControl.EmptyState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ public partial class QuerySessionControl : UserControl
/// <summary>
/// Decides whether the editor's empty state is showing, and rebuilds it when it is.
///
/// <para>Shown only when this session holds nothing at all: no text, and no sub-tab beyond
/// the Query Editor. Both halves matter — a session whose editor is empty because the user
/// is reading the plan they just ran must not have an overlay waiting behind that plan.</para>
/// <para>Shown only when this session holds nothing at all: no text, no sub-tab beyond
/// the Query Editor, and no server connection. All three halves matter — a session whose
/// editor is empty because the user is reading the plan they just ran must not have an
/// overlay waiting behind that plan, and a session that just connected is in use even
/// though nothing has been typed yet: the editor IS the offer now, and a panel still
/// suggesting "Connect to a server" over it reads as the connection having failed (#540).</para>
///
/// <para>Called from the editor's TextChanged and from the sub-tab watcher, so it re-decides
/// in both directions: delete every character with no plan open and the panel comes back,
/// which is the same state a fresh tab is in and deserves the same offer.</para>
/// <para>Called from the editor's TextChanged, from the sub-tab watcher, and from the
/// connect block, so it re-decides in both directions: delete every character with no plan
/// open and no connection and the panel comes back, which is the same state a fresh tab is
/// in and deserves the same offer.</para>
/// </summary>
private void RefreshEmptyState()
{
var empty = QueryEditor.Text.Length == 0 && !HasDocuments;
var empty = QueryEditor.Text.Length == 0 && !HasDocuments && _serverConnection == null;

if (empty)
{
Expand Down
Loading
Loading