Release v1.23.0 - #480
Merged
Merged
Release v1.23.0#480
Conversation
Plan tabs came back after a restart. Query tabs did not, even when the query had been opened from a file and the path was sitting on the session control. GetTabFilePath knew exactly one tab shape: a DockPanel with a PlanViewerControl inside it. A query tab is a QuerySessionControl with no wrapper, and it has had a SourceFilePath of its own since #459. It was simply never asked, so SaveOpenPlans wrote nothing down and RestoreOpenPlans had nothing to bring back. The same blind spot hid Copy Path on the tab context menu, which is shown only when GetTabFilePath answers. It had never appeared on a query tab. With both kinds of file in one saved list, restore routes on extension through the existing OpenFileByExtension instead of assuming a plan. Handing a .sql file to LoadPlanFile produced an "XML is not valid" box where the user's query should have been. The setting is renamed open_plans -> open_tabs now that it holds both. A file written by the previous version is still read: the old key deserializes into a migration-only property that is merged into OpenTabs on load and then nulled, so it drops out of the file on the next save rather than taking a user's restored tabs with it. Scope: this restores query tabs that came from a file. A scratch tab that was never saved has no path and still does not come back; persisting unsaved buffers is #462. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A query session tracked where it came from but never what was in it, so nothing in the app could tell an edited tab from an untouched one. Closing either lost the same amount of work: all of it, silently. The dirty flag is the feature; the close prompt and the modified marker are both consumers of it. QuerySessionControl now keeps the text as of the last load or save and compares against it, so typing something back to how it started leaves the session clean rather than latching modified on the first keystroke. Close now asks, on every path that discards a tab — the x, middle-click, the context menu, Ctrl+W — and on the window itself, which walks every tab rather than the one in front. Cancel cancels. A never-saved scratch tab answering Save routes through Save As, because saving it in place would write over nothing. The tab's x becomes a filled dot while there are unsaved changes and reverts to the x under the pointer, so a marked tab is still a closable one. Both glyphs are \u escapes, matching the "✕" they replaced. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (#470) Copying a statement out of a plan handed back @0, @1 ... where the literals had been. The text was correct — the reporter's database has PARAMETERIZATION FORCED, so SQL Server rewrote the statement before compiling it and the plan's StatementText carries the rewritten form — but a query full of undeclared parameters does not run, and running it is why you copy a statement out of a plan. The values were already in the plan. ParameterList carries a compiled and a runtime value for every one of them, and ShowParameters has been parsing and displaying them all along. Nothing new needed reading; the substitution was simply not offered anywhere. ParameterSubstitution.Apply does the rewrite: runtime value where present, compiled value otherwise, parentheses off numerics, quotes kept on strings, showplan's {guid'...'} braces off. It matches whole tokens against a scan that skips string literals, delimited identifiers, and comments, so @1 cannot land inside @11 and an @0 inside a literal is left where it is. A name with no captured value stays visible rather than being filled with an invention. The two existing menu entries produce the substituted form and say so in their labels; the plan's own text stays reachable through a third entry that appears only when the two forms differ. Ctrl+C on the statements grid follows the same path, since it is the unlabelled twin of Copy Query Text. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ShowError ended with an unconditional ShowDialog(this), which throws InvalidOperationException: "Cannot show window with non-visible owner" when the owner has not been shown yet. Both the command-line open and the restore of the previous session's tabs run from the MainWindow constructor, so a plan file that fails XML validation at startup died on the report instead of showing it. Same guard ShowFileError got in #459, plus a doc comment that says why it is there so the next person does not tidy it away. Test: ShowErrorBeforeVisibleTests drives LoadPlanFile against a never-shown MainWindow on both arms of ValidatePlanXml — malformed XML and well-formed XML that is not a plan. Red without the guard with the non-visible-owner exception, green with it. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
CreateTab decided Copy Path's visibility once, while the tab was being built, and never asked again. Open a scratch query and save it — the session gains a SourceFilePath, the tab is retitled after the file, and Copy Path is still not on the menu, because it was told there was nothing to copy back when that was true. Only a restart, which rebuilds the tab, put it right. The gate now recomputes on ContextMenu.Opening. The menu is only consulted when it opens, so that is the honest moment to answer, and it covers the other direction for free: a tab that stops having a file stops offering to copy one, rather than leaving a menu item that copies nothing when clicked. The tests drive the real gesture rather than the handler. Avalonia raises ContextMenu.Opening from its ContextRequested handler on the attached control, not from ContextMenu.Open() — calling Open() directly sets IsOpen and never asks — so they raise ContextRequestedEvent on the tab header, which is the path a right-click takes. Both fail against the one-shot code, each on the assertion after the change rather than the one before it. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#477) A query session detached into its own window dodged both of #462's prompts. The window's close path is DetachedWindowHelper's own and asked nobody anything, and while detached the session is out of MainTabControl.Items, so TabsWithUnsavedChanges() honestly reported nothing to save with a dirty edit sitting in another window. ShowDetached gains a closeGuard: a synchronous question asked once, before anything is destroyed. Null means close now with nothing asked - read-only content, an unmodified session, and app shutdown - so a plan or Query Store window takes exactly the path it always took. A task means cancel the close until it answers, the same cancel-then-reissue OnClosing uses (#462). MainWindow keeps a register of the query sessions living in detached windows, and UnsavedWorkOnClose() is now one walk over both the tab strip and that register rather than two walks, because two walks is how the second one gets forgotten. The shutdown prompt asks about detached sessions from ConfirmWindowCloseAsync, while every window is still up, rather than from OnClosed where they are force-closed after the main window is already gone. Claude-Session: https://claude.ai/code/session_017xj7HmCKrnsz2PWkRKT2Jx Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…478) A PlanViewerControl shown in a headless Window left every window built afterwards throwing KeyNotFoundException for fonts:SystemFonts. The test that did it passed, and which of the others went red depended on the order the runner picked that day. The session is not one Application per process, which is what the harness comment claimed. HeadlessUnitTestSession runs at PerTest isolation: each dispatch enters an AvaloniaLocator scope, builds an Application inside it, and tears both down afterwards. That teardown disposes the FontManager first and calls Dispatcher.ResetForUnitTests second, and ResetForUnitTests executes whatever is still queued. A window whose content left a deferred render pass behind therefore renders text against a font manager that was disposed a line earlier, throws, and the throw escapes the teardown delegate before it reaches scope.Dispose(). The scope is never popped. Every later dispatch nests inside the leaked one, resolves the disposed font manager through its parent chain, and dies constructing any Window at all. HeadlessUi.Run now drains the dispatcher queue at the end of every dispatch, so the teardown has nothing left to run against a font manager it has already disposed. It also follows each test with a one-object health check, so a future case that defeats the drain fails on the test that caused it instead of on the thirty that follow. StatementParameterMenuTests drops the workaround it was documenting: it now shows the control in a window and raises the context request a right-click raises, which is the path that decides the labels. UpdateStatementMenuForSelection goes back to being private. Claude-Session: https://claude.ai/code/session_016a1AnKAHwcALrwdYVVrpgR Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016a1AnKAHwcALrwdYVVrpgR Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v1.23.0.
Queries stop being second-class
Open Query and Save Query got the rest of what they needed. Query tabs now come back when the app restarts, the way plan tabs always have, and Copy Path appears on their context menu for the first time — both were the same missing branch in
GetTabFilePath(#463). Editing a query and closing the tab, the window, or a detached window now asks Save / Don't Save / Cancel instead of discarding the work silently, and a modified tab wears a dot in place of its × until you point at it (#462, #473).The detached-window half of that was the worse bug: a detached session is off the tab strip, so the shutdown prompt was walking tabs and honestly reporting nothing to save while a dirty edit sat in another window.
Copying a statement out of a plan gives you something you can run
Under
PARAMETERIZATION FORCED, stored procedures, orsp_executesql, the statement text a plan records is full of@0,@1— correct, and unrunnable. Copy Query Text and Open in Query Editor now put the plan's captured values back, runtime first and compiled as fallback, with (as parameterized) alongside for when you want the text that matches the plan cache. GridCtrl+Cproduces the same thing as the menu entry (#467).Fixes
ScrollViewerand assignsAllowAutoHideto its own bars in code (Stop autohiding scrollbars #464).Internal
The headless test harness could be poisoned by a single test leaving a render pass pending — Avalonia disposes the font manager before running queued jobs, and the resulting throw leaked a locator scope, failing every UI test that ran afterwards on something that was not its fault. Fixed at the harness, with a health check that now blames the guilty test (#474).
388 tests, 0 failures.
🤖 Generated with Claude Code
https://claude.ai/code/session_016a1AnKAHwcALrwdYVVrpgR