diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 96de992..a159972 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -86,10 +86,11 @@ All verified working via Phase 0 spike on SSMS 18, 20, and 22: ```csharp [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] +[ProvideAutoLoad(UIContextSsms, PackageAutoLoadFlags.BackgroundLoad)] // SSMS 22's own context [ProvideToolWindow(typeof(SqlPilotToolWindow))] ``` -The package loads automatically when SSMS starts via `ProvideAutoLoad`. +The package loads automatically when SSMS starts via `ProvideAutoLoad`. Two contexts are needed: `NoSolution` covers SSMS 18/20 and a plain SSMS 22 start, but an `ssms -S ` launch of SSMS 22 never raises it, so the shell's own `UICONTEXT_SSMS` is registered as well. The attributes are documentation here — the shipped `SqlPilot.Package.pkgdef` is hand-maintained and must carry both entries itself (see SSMS_INTEGRATION_NOTES.md § Autoload). ### IObjectExplorerService diff --git a/docs/SSMS_INTEGRATION_NOTES.md b/docs/SSMS_INTEGRATION_NOTES.md index 7b60423..2ac92e5 100644 --- a/docs/SSMS_INTEGRATION_NOTES.md +++ b/docs/SSMS_INTEGRATION_NOTES.md @@ -338,6 +338,55 @@ Start-Process 'C:\Program Files\Microsoft SQL Server Management Studio 22\Releas Note: SSMS 22 uses `-log` (dash), not `/log` (slash). SSMS 18 accepts both. +### SSMS 22's command line is not SSMS 18/20's + +Two differences that each cost a long debugging session: + +- **There is no `-E`.** SSMS 18/20 take `-E` for Windows auth; SSMS 22 dropped it (auth is + `-A `, and Windows auth is the default). An unknown switch does **not** start the + IDE — it shows a usage-error dialog whose title is *"Microsoft SQL Server Management Studio"*, + the same as the real main window. Automation that finds a window by title and process will + happily attach to that 625×420 error box, and every "the extension didn't load" conclusion + drawn from it is meaningless. Check the window is IDE-sized, or that Object Explorer exists, + before trusting anything else. +- **`-log` takes a filename** (`-log C:\path\ActivityLog.xml`). Bare `-log` consumes the next + argument as the filename, so `-log -S server` silently eats `-S`. + +### Autoload: NoSolution is not enough on SSMS 22 + +The package autoloads on `UIContextGuids80.NoSolution`. That fires at startup in the VS 2017 +shell (SSMS 18/20) and on a **plain** SSMS 22 start, but **not** when SSMS 22 is launched as +`ssms -S ` — the auto-connect path never raises it. Because `SqlPilotCommandSet.vsct` +is compiled by neither csproj, the Ctrl+D hotkey registered during package init is the only way +into the extension, so on that path it simply never appears, with no error anywhere. + +`SSMS.Application.pkgdef` declares the shell's own context, `UICONTEXT_SSMS = +{B7B07F42-6013-4C67-A504-C771CBC7625A}`, and autoloading on that as well covers the `-S` path. +Both triggers are in `SqlPilot.Package.pkgdef`; a package loads once however many fire. + +The pkgdef is **hand-maintained and checked in** — `Deploy-Dev.ps1` and `release.yml` copy +`src/SqlPilot.Package/SqlPilot.Package.pkgdef`, and the VSSDK pkgdef-generation target does +not run in this SDK-style project (same reason as the `.vsix` container target). Changing a +`[ProvideAutoLoad]` attribute does nothing on its own; edit the pkgdef too. + +**How to test an autoload context without rebuilding or elevation:** VSIXInstaller will install +a per-user `.vsix` whose only payload is a pkgdef adding one more `AutoLoadPackages` entry for +the already-registered package GUID. If the package then loads, that context fires; a control +probe with a made-up GUID must *not* make it load. This is how the two contexts above were +established. + +```powershell +# minimal probe pkgdef -- the package itself is already registered from Program Files +[$RootKey$\AutoLoadPackages\{B7B07F42-6013-4C67-A504-C771CBC7625A}] +"{8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b}"=dword:00000002 +# zip it with extension.vsixmanifest + [Content_Types].xml, then: +& "$ide\VSIXInstaller.exe" /quiet probe.vsix +& "$ide\VSIXInstaller.exe" /quiet /uninstall: +``` + +Plain file copies into `%LocalAppData%\Microsoft\SSMS\\Extensions\` are **not** picked up +(not even by `/updateconfiguration`); only VSIXInstaller registers per-user extensions. + ## Debug Logging `Debug.WriteLine` from an extension goes to... nowhere useful by default. For real diagnostics: @@ -349,6 +398,7 @@ For user-visible messages, use the SQL Pilot status bar (`IndexStatus.Text`). ## Things We Tried That Don't Work +- **`Process.Modules` as a "did the extension load?" check** — managed assemblies loaded by a package don't reliably appear there; it reported "not loaded" for an extension that was demonstrably running. Drive the real UI instead: send Ctrl+D and look for the `SQL Pilot` pane through UI Automation — after first confirming the window you attached to is the IDE and not a dialog (see *SSMS 22's command line*). - **DTE commands** for Edit Top N Rows: `Query.EditTopNRows`, `ObjectExplorer.EditTopNRows`, etc. — none exist in SSMS - **`DesignTableOrView(DocumentType.OpenTable)`** — fails at `GetDsRef` on all three SSMS versions. Use `OpenTableHelperClass.EditTopNRows` instead - **ScheduleSqlScriptAsOneStep** — opens the SQL Agent Job Schedule dialog, not a query diff --git a/src/SqlPilot.Package/SqlPilot.Package.pkgdef b/src/SqlPilot.Package/SqlPilot.Package.pkgdef index cb975e4..affa06b 100644 --- a/src/SqlPilot.Package/SqlPilot.Package.pkgdef +++ b/src/SqlPilot.Package/SqlPilot.Package.pkgdef @@ -5,9 +5,15 @@ "CodeBase"="$PackageFolder$\SqlPilot.Package.dll" "AllowsBackgroundLoad"=dword:00000001 +; NoSolution: raised at startup by the VS 2017 shell (SSMS 18/20) and by a plain SSMS 22 start. [$RootKey$\AutoLoadPackages\{adfc4e64-0397-11d1-9f4e-00a0c911004f}] "{8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b}"=dword:00000002 +; UICONTEXT_SSMS: SSMS 22's own context. An "ssms -S " launch never raises +; NoSolution, and this is the only other way the package gets loaded. +[$RootKey$\AutoLoadPackages\{B7B07F42-6013-4C67-A504-C771CBC7625A}] +"{8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b}"=dword:00000002 + [$RootKey$\ToolWindows\{c7d8e9f0-1a2b-3c4d-5e6f-7a8b9c0d1e2f}] @="SqlPilot.Package.SqlPilotToolWindow" "Package"="{8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b}" diff --git a/src/SqlPilot.Package/SqlPilotPackage.cs b/src/SqlPilot.Package/SqlPilotPackage.cs index f6d2c8f..a701244 100644 --- a/src/SqlPilot.Package/SqlPilotPackage.cs +++ b/src/SqlPilot.Package/SqlPilotPackage.cs @@ -19,13 +19,24 @@ namespace SqlPilot.Package [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [InstalledProductRegistration("SQL Pilot", "Quick Search Tool for SSMS", "1.0.0")] [Guid(PackageGuidString)] + // Two autoload triggers, because no single context fires everywhere. NoSolution is + // what the VS 2017 shell in SSMS 18/20 raises at startup. SSMS 22 raises it on a + // plain start too, but NOT when launched as "ssms -S " -- that auto-connect + // path skips it, and with no command table the Ctrl+D hotkey registered here is the + // only way in, so the extension simply never appeared. UICONTEXT_SSMS is the shell's + // own context (declared in SSMS.Application.pkgdef) and covers that path. A package + // loads once no matter how many of its contexts fire. [ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] + [ProvideAutoLoad(UIContextSsms, PackageAutoLoadFlags.BackgroundLoad)] [ProvideToolWindow(typeof(SqlPilotToolWindow), Style = VsDockStyle.Tabbed, Window = "d114938f-591c-46cf-a785-500a82d97410")] public sealed class SqlPilotPackage : AsyncPackage { public const string PackageGuidString = "8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b"; + /// SSMS 22's own startup UI context ("UICONTEXT_SSMS" in SSMS.Application.pkgdef). + private const string UIContextSsms = "B7B07F42-6013-4C67-A504-C771CBC7625A"; + internal static string DataDirectory { get; private set; } internal SearchEngine SearchEngine { get; private set; } internal FavoritesStore FavoritesStore { get; private set; }