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
2 changes: 1 addition & 1 deletion src/PlanViewer.App/Dialogs/SettingsWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ private void ResetAll_Click(object? sender, RoutedEventArgs e)
var fresh = new AppSettings
{
RecentPlans = _settings.RecentPlans,
OpenPlans = _settings.OpenPlans,
OpenTabs = _settings.OpenTabs,
AccuracyRatioDivergenceLimit = _settings.AccuracyRatioDivergenceLimit
};
_settings = fresh;
Expand Down
38 changes: 29 additions & 9 deletions src/PlanViewer.App/MainWindow.FileOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,43 +384,63 @@ private bool ValidatePlanXml(string xml, string label)
}

/// <summary>
/// Saves the file paths of all currently open file-based plan tabs.
/// The file behind every open tab that has one, in tab order. Plans and queries both,
/// since <see cref="GetTabFilePath"/> answers for either shape.
/// </summary>
private void SaveOpenPlans()
/// <remarks>
/// Separate from <see cref="SaveOpenPlans"/> so a test can assert what would be persisted
/// without writing over the user's real settings file.
/// </remarks>
internal List<string> CollectOpenTabPaths()
{
_appSettings.OpenPlans.Clear();
var paths = new List<string>();

foreach (var item in MainTabControl.Items)
{
if (item is not TabItem tab) continue;

var path = GetTabFilePath(tab);
if (!string.IsNullOrEmpty(path))
_appSettings.OpenPlans.Add(path);
paths.Add(path);
}

return paths;
}

/// <summary>
/// Saves the file paths of all currently open file-based tabs, plans and queries alike.
/// </summary>
private void SaveOpenPlans()
{
_appSettings.OpenTabs.Clear();
_appSettings.OpenTabs.AddRange(CollectOpenTabPaths());

AppSettingsService.Save(_appSettings);
}

/// <summary>
/// Restores plan tabs from the previous session. Skips files that no longer exist.
/// Restores the tabs from the previous session. Skips files that no longer exist.
/// Falls back to a new query tab if nothing was restored.
///
/// <para>The saved list holds queries as well as plans, so it routes on extension the
/// same way an ordinary file open does. Sending a .sql file to LoadPlanFile would greet
/// the user with "the XML is not valid" where their query used to be.</para>
/// </summary>
private void RestoreOpenPlans()
{
var restored = false;

foreach (var path in _appSettings.OpenPlans)
foreach (var path in _appSettings.OpenTabs)
{
if (File.Exists(path))
{
LoadPlanFile(path);
OpenFileByExtension(path);
restored = true;
}
}

// Clear the open plans list now that we've restored
_appSettings.OpenPlans.Clear();
// Clear the restored list now that its tabs are back on screen
_appSettings.OpenTabs.Clear();
AppSettingsService.Save(_appSettings);

if (!restored)
Expand Down
14 changes: 14 additions & 0 deletions src/PlanViewer.App/MainWindow.Tabs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ private static void SetTabLabel(TabItem tab, string label)
text.Text = label;
}

/// <summary>
/// The file a tab was opened from, or null when nothing on disk is behind it
/// (a pasted plan, a scratch query, a Query Store tab).
///
/// <para>Every tab shape that can carry a path has to be recognised here, because this one
/// method answers two questions: what <see cref="SaveOpenPlans"/> writes down for the next
/// session, and whether <b>Copy Path</b> appears on the tab's context menu. A shape it does
/// not know about loses both without saying anything.</para>
/// </summary>
private static string? GetTabFilePath(TabItem tab)
{
// Plans opened from file are wrapped in a DockPanel with the viewer as the last child
Expand All @@ -198,6 +207,11 @@ private static void SetTabLabel(TabItem tab, string label)
return v.SourceFilePath;
}
}

// Queries are the session control itself, with no wrapper around it
if (tab.Content is QuerySessionControl session)
return session.SourceFilePath;

return null;
}

Expand Down
36 changes: 35 additions & 1 deletion src/PlanViewer.App/Services/AppSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public static AppSettings Load()
settings = JsonSerializer.Deserialize<AppSettings>(json, JsonOptions) ?? new AppSettings();
}

// Settings written before the open-tab list held queries use the old key.
// Ahead of MigrateFormatSettings, which can Save mid-load and would otherwise
// write the old key straight back out.
MigrateOpenTabs(settings);

// Migrate legacy format settings file into unified settings
MigrateFormatSettings(settings);

Expand Down Expand Up @@ -117,6 +122,23 @@ public static void Save(AppSettings settings)
}
}

/// <summary>
/// Moves an "open_plans" list written by an older version onto <see cref="AppSettings.OpenTabs"/>,
/// so upgrading does not cost the user the tabs they had open. Only fills an empty OpenTabs:
/// if both keys are somehow present, the current one wins.
/// </summary>
/// <remarks>
/// Nothing is written here. The old key disappears from disk on the next ordinary save, which
/// happens on the first restore, so a downgrade before that point still finds its list.
/// </remarks>
internal static void MigrateOpenTabs(AppSettings settings)
{
if (settings.LegacyOpenPlans is { Count: > 0 } legacy && settings.OpenTabs.Count == 0)
settings.OpenTabs = legacy;

settings.LegacyOpenPlans = null;
}

/// <summary>
/// If the old perfstudio_format_settings.json exists, migrate it into AppSettings
/// (when FormatOptions is not yet set) and delete the old file unconditionally.
Expand Down Expand Up @@ -214,8 +236,20 @@ internal sealed class AppSettings
[JsonPropertyName("recent_plans")]
public List<string> RecentPlans { get; set; } = new();

/// <summary>
/// Paths of the tabs that were open when the app last closed, reopened on the next start.
/// Holds queries as well as plans, which is why it is no longer named for plans.
/// </summary>
[JsonPropertyName("open_tabs")]
public List<string> OpenTabs { get; set; } = new();

/// <summary>
/// What <see cref="OpenTabs"/> was called before it held queries too. Read on load so an
/// upgrade does not throw away the tabs the previous version wrote down, then nulled —
/// nulls are not serialized, so the old key drops out of the file on the next save.
/// </summary>
[JsonPropertyName("open_plans")]
public List<string> OpenPlans { get; set; } = new();
public List<string>? LegacyOpenPlans { get; set; }

/// <summary>
/// Divergence limit for accuracy ratio coloring on plan links. Default 10.
Expand Down
Loading
Loading