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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to the **GemStone Smalltalk** extension will be documented i

### Added

- **GemStone Search has a refresh button.** Its class, dictionary and global lists are loaded once when the panel opens and then kept up to date by the things that announce themselves — a compile from the IDE, a class removed in the Explorer, a commit or abort. Code created or removed by *executing* it — a `subclass:` or `compileMethod:` evaluated in a workspace, a global assigned in a doit — announces nothing, so it stayed invisible to the search until the next commit. A **⟳** button in the search panel (and in the panel's title bar, plus **GemStone: Refresh GemStone Search** in the Command Palette) reloads everything from the stone and re-runs what you have typed — including re-fetching an open senders/references list, which is just as stale as the rest. ([#517](https://github.com/GemTalk/Jasper/issues/517))
- **A session's stone and gem configuration now has its own panel, opened from the session row.** Seeing what a live session is actually running with — and changing the values that can be changed — was only possible inside the all-in-one GemStone Manager webview, which bundled connection, versions, databases, processes, logs and configuration into one surface; there was no way to look at configuration on its own, and nothing on the session row led to it. A gear on each session row in **Logins & Sessions** (also **GemStone: Session Configuration** in the Command Palette, which uses the active session) opens **Session Configuration** as an editor tab for *that* session, read over that session's own connection rather than whichever session happens to be active — so two sessions can be opened side by side and compared, and a panel closes itself when its session logs out. Each parameter says what you can do with it and why: config-file parameters (`ALL_CAPS`) are read-only because they were fixed before the stone started, a stone parameter is read-only unless you are SystemUser, and values of a kind that cannot be edited in place say so too — with the type and the purpose text from `system.conf` on an ⓘ that pins on click. The stone stays the authority on a change: its verdict comes back in its own words, beside the row that was changed, so a refused or silently-ignored set is visible rather than assumed. A filter box narrows a long report, Refresh re-reads from the session, and **Ping** moved off the session row into the panel — the row is losing buttons rather than gaining them. This is the first piece of the GemStone Manager to be pulled out into a surface of its own, with navigation staying in the sidebar. ([#511](https://github.com/GemTalk/Jasper/issues/511), [#232](https://github.com/GemTalk/Jasper/issues/232))
- **Breakpoints you can set, aim, disable and manage.** Setting a breakpoint used to mean a gutter click, which lands on the leftmost step point of the line — but a Smalltalk line routinely holds several, so the one you wanted was often not the one you got, and there was no way to disable a breakpoint, act on all of them at once, or see what the session actually had. Breakpoints now live in VS Code's own breakpoint list, so the gutter, the per-breakpoint checkbox and the built-in Enable/Disable/Remove All controls all drive GemStone through it. **Toggle Breakpoint at Cursor** (`Shift+F9`) breaks at the step point under the caret rather than the line's first, and the token that will actually break is outlined — grey and faded when disabled, matching the grey the gutter dot goes. A disabled breakpoint is kept in the gem as set-then-disabled, so re-arming it is instant. **Enable All**, **Disable All** and **Remove All Breakpoints** cover every GemStone breakpoint, including ones set outside Jasper. ([#262](https://github.com/GemTalk/Jasper/issues/262))
- **Step points are numbered where they are, without getting in the way.** Step point numbers are drawn as inlay hints — VS Code's own dim, recessive style, suppressed or restyled by its `editor.inlayHints.*` settings like any other — and each number is clickable to toggle a breakpoint at that step point. `gemstone.stepPoints.display` decides when they show: `debugging` (the default, so they appear while a debug session runs and stay out of the way while you are reading or writing code), `always`, or `off`, with **Toggle Step Point Numbers** to flip them. Whatever the setting, hovering a step point reports its number and breakpoint state with links to set, clear, enable or disable it. Numbers are withheld while a buffer has unsaved edits, since the stone's offsets no longer describe the text on screen and a wrong number is worse than none. ([#262](https://github.com/GemTalk/Jasper/issues/262))
Expand All @@ -28,6 +29,8 @@ All notable changes to the **GemStone Smalltalk** extension will be documented i

### Fixed

- **GemStone Search now finds the method you actually typed.** Searching for a common selector could miss its most obvious implementor: typing `at:` returned `instVarAt:put:` and `floatAt:put:` from a couple of incidental classes and no `Array>>at:` at all. The search asks the stone for a bounded number of matching selectors to keep searching-as-you-type fast, but the stone collected the *first* ones it walked into rather than the *best* ones — and with over a thousand selectors containing `at:`, the thirty-odd classes that implement `at:` itself sat far past the cut-off and were never sent. The scan now ranks before it truncates — the selector you typed first, then selectors starting with it, then selectors merely containing it — so what a cut-off drops is the least relevant tail. ([#517](https://github.com/GemTalk/Jasper/issues/517))
- **GemStone Search no longer answers out of the session you just left.** Making another session active left the search panel showing the previous session's results, under the query that produced them, with the previewed source alongside — and because the rows still looked live, opening one fetched a method from the session that was now current. Switching sessions (or logging out of the last one) now clears the panel and rebinds it, and an open Spotter tab is re-pointed at the new session instead of quietly searching the old one. ([#517](https://github.com/GemTalk/Jasper/issues/517))
- **A named breakpoint deleted while Jasper was still asking which class no longer comes back.** A function breakpoint typed as a bare selector has to be pinned to one class, and where several implement it Jasper asks. Deleting the row instead of answering left the picker still open, and answering it then re-created the breakpoint that had just been removed. The breakpoint is checked for still being there before it is converted. ([#262](https://github.com/GemTalk/Jasper/issues/262))
- **Step points are read in one round trip instead of three.** Source, step point offsets and selector ranges were three sequential calls to the stone, made synchronously on the extension host from the hover and the inlay hints — so the first hover on a freshly opened method stalled the editor for all three in a row. They now arrive in one reply, which is also less work for the stone, since it computed the first two to answer the third anyway. ([#262](https://github.com/GemTalk/Jasper/issues/262))
- **A method is named the same way everywhere.** Function-breakpoint messages and log lines wrote `Account class>>reset` while every other method list — Senders, Implementors, References, safe delete — wrote `Account class >> #reset`. They all use the one shared helper now. ([#262](https://github.com/GemTalk/Jasper/issues/262))
Expand Down
89 changes: 78 additions & 11 deletions client/src/omniSearch/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,43 @@ Global "search anything browsable" for the GemStone IDE — the Jasper answer to
| A class removed (Explorer → Remove Class) | `notifyClassRemoved` → `applyChange`, fired **once per class** because the delete takes the subtree | re-fetch per name; the lookup comes back empty and the entry drops |
| Dictionary add / remove / rename | `onSymbolListChanged` → `notifySessionSynced` | full `resync` |
| Commit / abort / file-in | `notifySessionSynced` | full `resync`, deferred while hidden |

Everything else — a global created by evaluating code, a class removed by another session — is
only picked up by the next commit/abort `resync`. That is the by-design staleness window.
| The user presses ⟳ / runs `gemstone.search.refresh` | `refresh` | full reload + the references list; deferred only while the docked panel is collapsed |

Everything else — a global created by evaluating code, a class or method compiled by a workspace
doit, a class removed by another session — announces nothing this panel can listen for, so short of
a commit or abort it is not picked up at all. That staleness window is by design but it has no
upper bound, which is why the **⟳ refresh** exists: it is the user's way to close it on demand,
without inventing a polling scheme or making every doit fire a corpus reload
([#517](https://github.com/GemTalk/Jasper/issues/517)). It clears any deferred sync rather than
paying for both. Pressing ⟳ in the chrome is never deferred — the click proves someone is looking —
but running the command while the docked panel is COLLAPSED is: its view is disposed, so the reload
would pay three image-wide executes to post results to nobody. That request is remembered
(`refreshPending`) and paid on the next reveal, so the panel you come back to is the fresh one you
asked for. The control lives in the webview chrome (so both surfaces have it) AND in the view's
title bar (so it is discoverable where VS Code users look for a refresh).

It is a SEPARATE engine call from `resync`, not the same one wired to a button, and the difference
is the pivot. `resync` deliberately leaves a references list alone — a commit is not a request to
disturb what you are reading — but that made the ⟳ look like a dead button for anyone who happened
to have one open: the corpora reloaded silently and the stale senders stayed on screen. So
`refresh` re-asks the stone who references the row the pivot was taken from (keeping whatever
filter is typed into it), and if that row is gone — its method or class deleted — it leaves the
pivot rather than keep showing senders of nothing.
- _Methods_: the selector space is too large to preload, so this provider queries the stone
**per search term** (debounced, min query length `methodMinQueryLength`), reusing the
`searchSelectors` machinery.
`searchSelectors` machinery. That scan is **bounded and ranked**: it walks every selector of every
class in the symbol list, sorting matches into three tiers — the selector IS the term, STARTS WITH
it, merely CONTAINS it — and returns them in that order, capped per tier. Ranking has to happen on
the SERVER because the cut-off does: the walk visits dictionaries in hash order, so a scan that
stopped at the first `limit` matches answered whichever classes it reached first, and a term as
common as `at:` filled its slice with `instVarAt:put:` and friends while `Array>>at:` — the row the
user wanted — was never sent
([#517](https://github.com/GemTalk/Jasper/issues/517)). The client re-ranks what it receives with
the configured matcher, but it can only order rows it was given. The walk no longer short-circuits
on a match count (a better-tier hit can be anywhere in the image); the one early exit left is a
FULL exact tier, where nothing later can displace a row. Cost on a 3.6.2 base image: ~27 ms for
`at:`, against ~2 ms for the old early exit — and a full walk was already the price of every
precise term, which never reached the old cutoff at all.
- **Explicit-only** categories (`OmniCategory.explicitOnly`) are **excluded from the all-scope
fan-out** — they run only when the user scopes to them, so heavyweight work never fires on a plain
search:
Expand Down Expand Up @@ -166,6 +197,38 @@ Global "search anything browsable" for the GemStone IDE — the Jasper answer to
the current term. (The origin QuickPick could only express scope with cramped icon title buttons +
the title text — the webview tabs are the intended affordance.)

8. **A search belongs to one session, and says so when that changes.** Both hosts hold an engine built
from ONE session's deps — its providers, its activation and its source preview all close over that
session — plus a webview full of rows read out of it. Nothing used to react when the user made
another session active: the docked panel rebuilt its engine only when something else happened to ask
it to (a reveal, a settings change, the next keystroke), and an open Spotter never rebuilt at all.
Until then the panel kept answering, and opening rows, out of a session the user had left
([#517](https://github.com/GemTalk/Jasper/issues/517)).

`SessionManager.onDidChangeSelection` now reaches both hosts. Two decisions worth recording:

- **The webview is wiped, not just the engine.** Dropping the engine is enough for a settings change,
because the rows on screen are still true. Here they are not: they came from the old session, yet
they still look live, and activating one would open a document against the session that is now
current — a wrong answer presented as a right one. So the query, the results, any references pivot
and the preview all go, and the scope returns to All (where the replacement engine starts).
- **The wipe is NOT deferred while hidden, though the rebuild still is.** The `visible` gate exists to
avoid paying for image-wide GCI executes on a background path, and that reasoning still holds for
re-priming. It does not hold for clearing the screen: a reveal cannot un-show stale rows
retroactively, so the cheap part happens immediately and only the expensive part waits.

The Spotter is **re-pointed in place** rather than closed and reopened: it is an editor tab the user
put there (possibly pinned), and the tab, its pin and its loaded HTML are all session-independent.
`show()` for a different session takes the same path, which removed its old dispose-and-recreate
branch. Logging out of the last session is the same event with nothing to bind to: both hosts reset
and say "Log in to a GemStone session to search" instead of leaving the departed session's rows up —
and both DROP THE ENGINE as well. (The docked panel is always still there to see this; an UNPINNED
Spotter has already disposed itself on focus-out by the time you reach the logout, so only a pinned
one takes this path.) Clearing the screen alone would leave the departed session's primed
corpora (and an `activate` closed over its GCI handle) one keystroke away: the docked host's
`ensureEngine` gate already refused to answer without one, and the Spotter now refuses the same way,
showing the notice instead of searching. A later login rebinds both.

## Module map (`client/src/omniSearch/`)

| File | Responsibility | Stone? | Tested |
Expand Down Expand Up @@ -200,8 +263,8 @@ New shared query (if needed) lives under `client/src/queries/` per repo conventi
- `categories`: which providers are enabled (default: all seven —
`classes, methods, dictionaries, globals, source, literals, categories`).
- `maxResultsPerCategory`: number (default `20`) — how many rows are **shown** per scope.
- `maxServerScan`: number (default `200`, clamped 20–20 000) — how many matches a scope's
**server-side scan** collects before it stops. A different bound from `maxResultsPerCategory`; see
- `maxServerScan`: number (default `200`, clamped 20–20 000) — the most matches a scope's
**server-side scan** hands back. A different bound from `maxResultsPerCategory`; see
"Two different limits bound a result set" below.
- `debounceMs`: number (default `120`).
- `methodMinQueryLength`: number (default `2`) — min chars before the Methods provider queries the stone.
Expand Down Expand Up @@ -241,11 +304,15 @@ Behaviour decisions (Eric's review of the first webview cut):
## Two different limits bound a result set

The display cap (`maxResultsPerCategory`, raised by Load-more/Load-all) is not the only bound — the
**Methods** scope also has a server-side one. `searchSelectors` short-circuits the moment it has
`limit` matches, and `methodsProvider` clamps that limit to `maxServerScan` (default 200) however high
the display cap goes. So with the default a broad selector term can never yield more than 200 rows,
Load-all included. That ceiling is a **setting** rather than a constant precisely because the honest
answer to "I want more than 200" is "raise the scan, and accept a slower search".
**Methods** scope also has a server-side one. `searchSelectors` yields at most `limit` rows, and
`methodsProvider` clamps that limit to `maxServerScan` (default 200) however high the display cap goes.
So with the default a broad selector term can never yield more than 200 rows, Load-all included. That
ceiling is a **setting** rather than a constant precisely because the honest answer to "I want more than
200" is "raise the scan, and accept a slower search".

What the ceiling drops is the least relevant tail, not an arbitrary slice: the scan ranks by match tier
before it truncates (decision 3), so the exact and prefix hits survive a cut-off that would once have
discarded them unseen.

The two bounds mean different things to the user, so a provider reports when its OWN ceiling was the
one that bound it (`OmniTruncationSink`, an optional 4th argument to `OmniProvider.search`, carrying
Expand Down
Loading