Skip to content

Add per-server and per-database search scope - #8

Merged
mourier merged 8 commits into
mainfrom
feature/search-scope
Sep 21, 2026
Merged

mourier merged 8 commits into
mainfrom
feature/search-scope

Conversation

@mourier

@mourier mourier commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Closes #5.

Adds a Scope panel (filter icon next to Refresh, or Alt+S) with a checkbox tree of every connected server and its databases. Unchecking a database drops it from the index and from search results immediately; unchecking a server drops all of it. Re-checking spot-indexes just that database — no full re-index. Exclusions persist across SSMS restarts, and a database that appears later on an in-scope server is indexed automatically. The status line reads Indexed N objects in X of Y database(s) from A of B server(s) and collapses to the old wording when nothing is excluded.

Design

  • Default-include, exclusion-list persistence. %AppData%\SqlPilot\scope.txt stores only exclusions (S|<server>, D|<server>|<database>), so an empty or missing file behaves exactly like today. Nothing changes for existing users until they uncheck something.
  • Scope is engine state, not a per-query filter. SearchEngine takes an optional ISearchScopeStore as a third constructor argument and enforces it at both index time (RefreshIndexAsync skips excluded databases) and search time (out-of-scope buckets are skipped). SearchFilter is unchanged.
  • Kept out of SqlPilotSettings and the Tools > Options page on purpose — it's dynamic state keyed to whatever happens to be connected.
  • Shared ScopeControl (SqlPilot.UI) drives both the SSMS tool window and the demo app; every checkbox and tree item has an automation name for screen readers.

Also in here

  • LineStore.LoadSettings split keys at the first | even when escaped — a latent bug the composite scope keys exposed. Fixed with escape-aware Join/Split, and LoadObjects now uses the same scheme.
  • A queued progress update could overwrite the final index status, leaving Indexing … on screen; visible on a scope re-check. Root cause was posting to the dispatcher from a continuation already on the UI thread — now a direct assignment.
  • Search box placeholder said Ctrl+Shift+D; the working hotkey is Ctrl+D (the .vsct that declared the other binding is compiled by neither csproj).

Verification

Verified in real SSMS 18, 20 and 22 against LocalDB through UI Automation: exclude / re-check / server toggle / tri-state, scope.txt round trip across a restart, a database created while SSMS was down showing up checked and indexed on Refresh, Alt+S focus handling, and search results filtering accordingly. Also driven from the demo app. Core tests go from 44 to 83.

A Scope button on the tool window toolbar opens a checkbox tree of the
connected servers and their databases. Unchecking a database drops it from
search results immediately and stops it being indexed; re-checking spot-indexes
just that database rather than triggering a full refresh.

Scope is an exclusion list (default-include), persisted to
%AppData%\SqlPilot\scope.txt, so an empty store behaves exactly as before and
new databases on an in-scope server are still picked up automatically.

- SqlPilot.Core/Scope: ISearchScopeStore + SearchScopeStore, mirroring
  FavoritesStore. Composite keys "S|<server>" and "D|<server>|<database>".
- SearchEngine takes an optional ISearchScopeStore and skips out-of-scope index
  buckets; adds ClearDatabase and GetIndexedDatabaseCount.
- LineStore.LoadSettings now splits at the first *unescaped* separator — a plain
  IndexOf split the new composite keys mid-key.
- RefreshIndexAsync filters each server's database list through the store before
  building index tasks. Database names are still enumerated for out-of-scope
  servers (one cheap query) to keep the tree and status line accurate.
- Status line reports "x of y database(s) from n of m server(s)" when scope
  excludes anything, and the old wording when it doesn't.
- UI.Demo goes multi-server so the tree is exercisable without SSMS.
Cleanup pass over the scope feature — no behaviour change.

- Extract the scope TreeView into SqlPilot.UI/Controls/ScopeControl, following
  the SearchControl pattern. It was copy-pasted into the tool window and the
  demo, 24 of 28 lines identical. The control is theme-neutral so each host
  keeps its own colours.
- Give LineStore the escape scheme it already owned: Join/Split replace
  SearchScopeStore's private copy of Esc/Unesc plus a hand-rolled splitter.
  The on-disk format is unchanged.
- Move the scope guard into SearchEngine.RefreshIndexAsync. The engine holds the
  store, so callers can no longer forget to filter.
- Collapse RefreshIndexAsync's per-server body, SpotIndexServerAsync and
  SpotIndexDatabaseAsync into one IndexServerAsync + a CreateProvider helper;
  same in the demo.
- Re-checking a server now reuses the database list already in the tree instead
  of a fresh connect + metadata query, and an out-of-scope server we've already
  seen isn't re-enumerated at all.
- Drop GetIndexedDatabaseCount: it was on ISearchEngine with no caller but a
  test. The status line derives its counts from the scope tree.
- Correct the comment claiming excluded buckets stay indexed (hosts drop them),
  and scope the store's class comment to the storage rule it actually owns.
- Every scope checkbox now carries AutomationProperties.Name/LabeledBy and the
  tree items carry a Name. The labels are siblings of the checkboxes, so a
  screen reader previously announced 16 unnamed "checkbox, checked" nodes and
  each tree item read as "SqlPilot.UI.ViewModels.ServerScopeNode".
- Alt+S toggles the scope panel, focusing the tree when it opens and returning
  to the search box when it closes. Registered as an in-window KeyBinding, not
  a global hotkey, so it can't clash with an SSMS or Windows shortcut.
- The Scope button is now the Segoe MDL2 filter glyph, matching the icon set
  ObjectTypeToIconConverter already uses. Icon-only, hence the explicit
  automation name and tooltip.
- Move RelayInputCommand to SqlPilot.UI so both hosts can use it. The copy in
  SqlPilot.Package was unused and absent from the Legacy csproj, so calling it
  from the shared tool window would have broken the SSMS 18/20 build.
- Fix the search placeholder, which advertised Ctrl+Shift+D while the code and
  README both say Ctrl+D.
Cleanup pass over the previous commit — no behaviour change.

- Delete RelayInputCommand. CommunityToolkit.Mvvm is already a PackageReference
  of SqlPilot.UI, already on build/SqlPilotDlls.txt, and its RelayCommand(Action)
  has identical semantics. Moving the type was the right direction; deleting it
  was the right depth.
- Drop AutomationProperties.LabeledBy and the two x:Name labels feeding it.
  FrameworkElementAutomationPeer.GetNameCore only consults LabeledBy when Name is
  empty, and Name is always set here, so it was never read.
- Drop AutomationProperties.HelpText. GetHelpTextCore falls back to ToolTip only
  when HelpText is empty, so the literal was hiding the better string: the Scope
  button now reports "(Alt+S)" and each database reports its server, which is
  what tells two same-named databases apart.
- Drop the server checkbox ToolTip, which repeated the label beside it.
- Replace both ScopeButton_Toggled handlers with a Visibility binding on
  IsChecked, deleting the duplicated code-behind in each host.
- FocusTree: check for an empty tree before forcing a layout pass, since that
  case never needed a container.
IndexDatabasesAsync posts per-database progress with a fire-and-forget
Dispatcher.BeginInvoke, so a late one could run after the caller had already
written "Indexed N objects in ...", leaving the stale "Indexing ..." text on
screen until the next refresh. Drain the queue below their priority before
returning.

Pre-existing, but the scope re-check path made it reliable: with a single
database the spot-index finishes fast enough that the stale text always won.

Also record the SSMS 22 findings from verifying this against real installs:
the package registers but never loads there (SSMS 18/20 are fine), and
Process.Modules is not a valid check for whether an extension loaded.
IndexDatabasesAsync is entered from the UI thread and none of its awaits leave
it, so the progress update was posting from the UI thread to the UI thread.
That post is what could outrun the caller's final status, so assigning directly
removes the race rather than draining the queue behind it, and the drain added
in 7a1eb9a goes away with it.

MergeServer now asks the store IsDatabaseIncluded instead of rebuilding that
rule from the raw exclusion list, and matches existing nodes through a
dictionary rather than a scan per database. New databases are inserted at their
sorted position, so the re-sort pass over a live-bound collection is gone.

DatabaseScopeNode drops the _isSyncing flag for a hand-written property: the
silent path writes the field, the bound path means the user clicked.

Also: LoadObjects now shares Split, so an object whose name contains "|" round
trips like the settings file already did; SearchAsync only parses a bucket key
when a filter or scope will read it; and the demo stops re-filtering by scope
ahead of the engine that owns the same store.
Every SSMS 22 launch behind that section had been started with -E, a switch
SSMS 22 no longer has. The window I attached to was the usage-error dialog,
which shares the IDE's title, so nothing had loaded because the IDE had not
started. The corrected findings live in the fix/ssms22-autoload-context branch.
The Process.Modules warning stands and stays.
Both branches added the same bullet at the same line; the autoload branch
carries the fuller version alongside the SSMS 22 command-line notes.
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: c260f4ac-3dd6-4e79-9013-564ded28b3c4

📥 Commits

Reviewing files that changed from the base of the PR and between 348c0be and 366ea49.

📒 Files selected for processing (23)
  • README.md
  • docs/ARCHITECTURE.md
  • src/SqlPilot.Core/Persistence/LineStore.cs
  • src/SqlPilot.Core/Scope/ISearchScopeStore.cs
  • src/SqlPilot.Core/Scope/SearchScopeStore.cs
  • src/SqlPilot.Core/Search/ISearchEngine.cs
  • src/SqlPilot.Core/Search/SearchEngine.cs
  • src/SqlPilot.Package/Commands/RelayInputCommand.cs
  • src/SqlPilot.Package/SqlPilotPackage.cs
  • src/SqlPilot.Package/SqlPilotToolWindowControl.xaml
  • src/SqlPilot.Package/SqlPilotToolWindowControl.xaml.cs
  • src/SqlPilot.UI.Demo/MainWindow.xaml
  • src/SqlPilot.UI.Demo/MainWindow.xaml.cs
  • src/SqlPilot.UI.Demo/MockDatabaseObjectProvider.cs
  • src/SqlPilot.UI/Controls/ScopeControl.xaml
  • src/SqlPilot.UI/Controls/ScopeControl.xaml.cs
  • src/SqlPilot.UI/Controls/SearchControl.xaml
  • src/SqlPilot.UI/ViewModels/DatabaseScopeNode.cs
  • src/SqlPilot.UI/ViewModels/ScopeViewModel.cs
  • src/SqlPilot.UI/ViewModels/ServerScopeNode.cs
  • tests/SqlPilot.Core.Tests/LineStoreTests.cs
  • tests/SqlPilot.Core.Tests/SearchEngineScopeTests.cs
  • tests/SqlPilot.Core.Tests/SearchScopeStoreTests.cs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mourier
mourier merged commit 63001fd into main Sep 21, 2026
2 checks passed
@mourier
mourier deleted the feature/search-scope branch September 21, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The good idea, but some notes

1 participant