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
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <server>` 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

Expand Down
50 changes: 50 additions & 0 deletions docs/SSMS_INTEGRATION_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <method>`, 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 <server>` — 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:<identity>
```

Plain file copies into `%LocalAppData%\Microsoft\SSMS\<ver>\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:
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/SqlPilot.Package/SqlPilot.Package.pkgdef
Original file line number Diff line number Diff line change
Expand Up @@ -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 <server>" 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}"
Expand Down
11 changes: 11 additions & 0 deletions src/SqlPilot.Package/SqlPilotPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <server>" -- 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";

/// <summary>SSMS 22's own startup UI context ("UICONTEXT_SSMS" in SSMS.Application.pkgdef).</summary>
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; }
Expand Down
Loading