Breakpoints: aim at a step point, enable/disable, and a manager view - #481
Conversation
Setting a breakpoint meant a gutter click, which lands on the leftmost step point of the line. 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. Make VS Code's own breakpoint list the durable model. GemStone method breakpoints are per-gem VM state: they do not survive logout and a commit does not persist them (verified against 3.6.2 and 3.7.5), so the stone can only ever say what one session currently has, never what the developer wants. With VS Code's list as the record, breakpoints survive a restart and the gutter, the per-breakpoint checkbox and the built-in Enable/Disable/Remove All controls all drive GemStone through onDidChangeBreakpoints. Jasper re-applies them on login and after a recompile. Step point precision rides on the breakpoint's column: a gutter click carries none and means the leftmost step point on the line, while an inline breakpoint or Toggle Breakpoint at Cursor (Shift+F9) carries the exact column and picks the step point nearest it. The token that will actually break is outlined, dashed when disabled. A disabled breakpoint is applied as set-then-disable, because disableBreakAtStepPoint: is a no-op on a step point with no breakpoint. Number step points as inlay hints — VS Code's own dim style, suppressed by its editor.inlayHints.* settings like any other — each clickable to toggle a breakpoint there. gemstone.stepPoints.display defaults to `debugging`, so the numbers appear while a debug session runs and stay out of the way while you are reading or writing code. Hovering a step point always reports its number and breakpoint state whatever the setting. Numbers are withheld while a buffer has unsaved edits, since the stone's offsets no longer describe the text on screen. Add a Breakpoints view listing what the gem actually holds, grouped by class and method with the step point each breakpoint resolved to and a checkbox per row. It is the gem's truth rather than a copy of VS Code's list, so it also surfaces breakpoints Jasper never set — from topaz, or a halt left in the code — which were invisible until execution stopped on one. Unify the two managers that fought each other: the cursor-toggle command kept its breakpoints in a private map, so they never appeared in the Breakpoints view and were silently wiped whenever a gutter breakpoint on the same method was applied. It now adds a normal VS Code breakpoint, so the two kinds are one kind. Two bugs found on the way: - Line-to-step-point mapping compared GemStone's 1-based source offsets against 0-based line offsets, so a step point on a line boundary was read as belonging to the previous line. Run to Cursor already corrected for this; the gutter path did not. - _allMethodBreakpoints' tuple stride is version-dependent — 3 fields on 3.6.2, 4 on 3.7.5, which gained breakpointLevel — so decoding it by hand walks off the end of the array on the older release. getAllBreakpoints reads the kernel's own _breakReport: instead, which knows its own stride. It also skips breakpoints stranded on a superseded method version, which a recompile leaves behind and the gem goes on reporting. The GemStone semantics all of this rests on are pinned by an integration test against a live stone, since none of them is documented anywhere we control. Renames gemstone.toggleSelectorBreakpoint to gemstone.breakpoints.toggleAtCursor with no alias, so a custom keybinding naming the old id needs the new one. Closes #262 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggle/Enable/Disable Breakpoint at Cursor returned silently in four cases — a non-method editor, an unsaved buffer, a diff view, and a method whose step points could not be read. These are all invoked deliberately, from a keystroke or a menu, so a silent no-op is unreadable: it looks exactly like a dead keybinding, and the developer has no way to tell "your buffer is unsaved" from "the command never ran". Give StepPointModel an `explain` that answers either the step points or the reason there are none, phrased as something the developer can act on, and report it. `get` stays as the quiet wrapper for the hover and the inlay hints, which have nothing useful to say about a method without step points and must not nag. The unsaved-buffer case is the one most likely to be hit and least likely to be guessed: step point offsets describe the compiled method, so acting on a modified buffer would put the breakpoint somewhere other than where the caret is pointing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VS Code persists its breakpoint list across restarts and restores it at startup — silently, without firing onDidChangeBreakpoints, and before any session exists. Nothing then pushed those breakpoints into a gem, so reopening the window left a red gutter marker with nothing behind it: the marker claimed execution would stop somewhere it wouldn't, and running the method quietly did not break. reapplyAll existed for exactly this but was only ever reachable from the manual "Re-apply Breakpoints to Session" command. Hook it to onDidChangeSelection, which a login fires, and scope it to breakpoints whose URI names that session. Method URIs carry the session id, so an unscoped re-apply would push one session's breakpoints into another session's gem — a breakpoint in a stone the developer never asked about. Session ids restart at 1 per window, so the usual first login reclaims a restored breakpoint; one naming a session that doesn't exist waits rather than leaking. reapplyAll now returns the number of methods it applied, so the manual command can say what it did instead of appearing to do nothing when there was nothing to do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A GemStone breakpoint lives in the gem, so it cannot outlive the session. VS Code persists its breakpoint list across restarts anyway — right for a file, wrong for a gem — so a reopened window showed a red marker for a breakpoint that existed nowhere. The previous commit made that marker true by re-applying it to whichever session logged in next; that was the wrong fix. It resurrects a breakpoint into a gem the developer never pointed it at, and it treats a transient thing as durable. Drop them instead: - logging out removes that session's breakpoints from VS Code's list, scoped by the session id in the method URI so another session's are untouched; - activation prunes anything a startup restore brought back, and an `added` event prunes a restore that lands later or a gutter click in a stale editor. Pruning is idempotent, so the removal it triggers re-enters the handler with nothing left to prune and cannot loop. Removes "Re-apply Breakpoints to Session", whose only purpose was to serve the persistence model being dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The marker that says *which* step point a breakpoint sits on distinguished disabled from enabled by border style alone — dashed versus solid, 1px, in a grey that some themes render nearly invisibly. Nobody notices that, so "is this breakpoint live?" — the one question the marker exists to answer — was unanswerable by looking. Lead with colour instead, the same red-versus-grey pair VS Code uses for the gutter dot, and fade the disabled token so the cue survives a theme where the grey border is lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toggle Step Point Numbers was Command Palette only, so turning the numbers on meant leaving the source you were reading them against. Put it on the editor title bar as a one-click button and on the editor right-click menu, next to the other breakpoint actions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The + button in VS Code's Breakpoints panel creates a function breakpoint — named rather than located. Jasper only implements source breakpoints, so one sat in the panel looking exactly like a working breakpoint and never fired. Unsaid, that reads as "breakpoints are broken" rather than "this kind isn't wired up". Warn on add. Deliberately does not delete it: the developer typed it on purpose, and silently removing what someone just typed is its own kind of confusing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The + button in VS Code's Breakpoints panel takes a method name rather than a location, and VS Code leaves resolving that name entirely to the debugger — so one sat in the panel indistinguishable from a working breakpoint and never fired. The previous commit only warned about it; naming a method you want to stop in, without going to find it first, is worth actually supporting. A bare selector is looked up across the image and, when more than one class implements it, the developer picks which one. A qualified `Account>>balance` or `Account class>>new` skips the prompt, but is still checked against the image rather than trusted — a typo would otherwise set a breakpoint that silently never fires, which is the failure this whole change is about. The name is then **converted** into an ordinary SourceBreakpoint on the method's first step point, rather than carried through the model as a second kind of breakpoint. That gives it a real location and red dot, enable/disable, death with its session and a row in the GemStone Breakpoints view, all for free — and avoids a named and a located breakpoint on one method fighting over which of them owns its breaks, since applying a method's breakpoints clears the method first. A name that resolves to nothing is dropped with an explanation rather than left sitting there, which is the same reasoning: an inert breakpoint that looks live is worse than no breakpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
handleAdded is fired without awaiting, since resolving a name can prompt. Any error inside it therefore became an unhandled rejection: the breakpoint stayed in the panel, unconverted and inert, with nothing said — the very failure the resolver exists to remove. Catch and report, and trace each decision (parsed name, candidates found, chosen class, resulting URI) to the GemStone log so a failure in the field is diagnosable without a debugger. Also guard the one input that would build a broken URI: implementorsOf reports no dictionary for a class not bound under its own name in the symbol list, and an empty dictionary segment yields a URI the file system provider cannot resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VS Code's + button in the Breakpoints panel creates the function breakpoint *first*, with an empty name, and only then opens it for inline editing. Two consequences, both wrong here: - the empty name arrived as an addition and was rejected as unresolvable, which removed the row and popped a warning before a single character could be typed; - the name the developer actually types arrives as a **change**, not an addition, so watching additions alone never saw it. Treat a blank name as "still being typed" and leave it strictly alone, and resolve from added and changed together. Editing an existing breakpoint's name now works for the same reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The filled-dot button in the Breakpoints panel header greys every breakpoint out, which reads as "these are off now". But `vscode.debug` exposes no activation state and no event for it, so an extension cannot observe the flag: Jasper's breakpoints stay armed in the gem and execution still stops on them. It sits one icon away from controls that do work, so it is worth naming rather than leaving to be discovered mid-debug. Disable All Breakpoints is the one that disarms them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`gemstone.maxEnvironment` is a ceiling, not a selection, and the resolver passed it straight to implementorsOf as the environment to search. On a stone configured above 0 that skipped environment 0 — where practically every method lives — so a perfectly ordinary selector came back as "Nothing implements #usesPrimary" while the Explorer listed it two panes away. Sweep 0..maxEnvironment and dedupe by class and selector, which is what the standalone senders and implementors commands already do. Each candidate now carries the environment it was found in, so the breakpoint is set against that environment rather than the configured ceiling — the same bug one step later. Verified against the dev stone: implementorsOf itself was answering correctly all along. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-applying them to the new method was wrong twice over. The recompile replaces the GsNMethod, so the gem's breakpoints on the old one are unreachable — and the new method's step point 4 may be an entirely different expression after an edit, so re-applying moves the breakpoint somewhere the developer never put it. The expectation is simply that a recompile clears them. Drop them from VS Code's list as well as the gem, so the gutter, the Breakpoints panel and the GemStone Breakpoints view agree — the same rule that already applies when a session logs out. The step point cache is still invalidated, since the offsets have moved either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two claims were left over from the first design, both now false: that VS Code's breakpoint list is a durable record surviving a restart (breakpoints die with the session), and that a disabled breakpoint's marker is dashed (it is grey and faded — the dashed border was too subtle to see). Also reorder the section so the headline feature leads rather than sitting mid-list behind the lifecycle rules, and move the Deactivate Breakpoints limitation under Documentation, which is what it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VS Code's Edit Breakpoint accepts a condition, a hit count or a log message, and honouring them is the debugger's job. Jasper does not implement any of the three, so the breakpoint stopped every time it was reached with nothing said — the worst shape this failure takes, because the developer wrote down a precise intent and the UI accepted it. Warn instead. The fields keep riding along through enable/disable and name-conversion, so nothing is lost when conditional breakpoints arrive (#277). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CHANGELOG conflict is a plain union: both sides added to [Unreleased], so every entry is kept and the headings are put back in Keep a Changelog order, which main's section had drifted from. Main's safe-delete work gave MethodSearchResult a required environmentId. This branch had been carrying that field itself, as a Candidate type that intersected the search row with an environment because the row had none — so the type is now a no-op and its comment is untrue. Candidate goes, the row is used directly, and the environment stamp the sweep applied on top of each result goes with it: implementorsOf already stamps the row with the environment it searched. The test's mocks now do the same, since a test asserting "found in environment 1" should not be asserting against a row that says 0. Main also added a scan that fails the build on a dedup key which leaves the environment out, with an allowlist for the choosers that fold a selector to one row on purpose. The class picker behind a named breakpoint is one of those — one class is one entry, the lowest environment wins, and that environment rides along to where the breakpoint is set — so it joins GemStone Search on the allowlist, with the reason recorded at both ends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the CHANGELOG conflicts, and only because both sides added under [Unreleased]. Every entry is kept, in Keep a Changelog heading order. The external-server work does not touch the query layer this branch builds on, so nothing needed reconciling beyond that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No conflicts, and nothing on this side needed changing: the compile-check work lands in the refactoring engine and its payload, which this branch does not touch. The engine payload moved, so the test stone's copy predates the merge. Reinstalled it before running the gate -- otherwise the engine-gated suites would have been testing the old engine and passing on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release moved everything out of [Unreleased] and under a version heading, so a union of the two sides would have been wrong twice: it would have pulled 16 already-shipped entries back into [Unreleased], and left this branch's own entries inside a release they were never part of. Resolved by taking main's file and putting back only the entries this branch has under [Unreleased] that do not appear in 1.8.14 -- the ten that describe the breakpoint work, which has not shipped. The version bump comes from main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two dependency bumps, package.json and package-lock only; nothing on this side needed changing. Both are majors, so both were actually exercised rather than assumed: eslint runs clean under globals 17, and the suite is green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
npapagna
left a comment
There was a problem hiding this comment.
Thanks @ericwinger! Left a couple of comments.
A breakpoint names a step point in a method that lives in the gem, so the positions it is placed by only mean something where the editor's text *is* that compiled method's. Two paths did not hold to that. Ad-hoc executed code. A stack frame with no gemstone:// path is either an "Executed Code" doit or a method whose class is not bound in the symbol list. Neither has anything durable to arm -- a doit's method is compiled for one execution and gone afterwards -- but the debug adapter tried anyway, on a best-effort line match that ignored the requested column, bypassed BreakpointManager entirely (so the result was invisible to the breakpoint view, Enable All, Disable All and pruning), and answered an unknown source reference with an empty list, which VS Code reads as acceptance. It is now refused per breakpoint with a reason, so the marker greys out and hovering it says why. mapLineToStepPoint went with it; the branch was its only caller. Unsaved edits. Step point offsets describe the compiled method while VS Code shifts its own breakpoints as the buffer is edited, so a dirty editor makes the two describe different code. Toggle Breakpoint at Cursor already refused on that basis; the gutter and the debug adapter did not. Because breakpoints are applied as an absolute model -- clear the method, then re-arm the whole list by position -- adding one to a dirty editor silently re-resolved every breakpoint on that method against offsets that no longer matched, moving ones the developer had not touched. A dirty editor now refuses a new breakpoint, taking it back out of the list, and the gem is left untouched meanwhile, so reverting the editor leaves the original breakpoints exactly where they were. Once it is clean again the gem catches up with anything the list did during the hold. Both refusals name the two ways on: save the method, or File: Revert File. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ured VS Code decides where its breakpoint gutter appears by language, with no way to narrow that by document -- and one language, gemstone-smalltalk, serves four different things: gemstone:// method editors, workspaces, .gst files, and the read-only source of a stack frame. Only the first can carry a breakpoint. The gutter was offered in all four, and a breakpoint set in the other three was dropped on the floor: a solid red dot that armed nothing, stopped nothing, and said nothing, which is indistinguishable from a breakpoint that simply never gets hit. The stack frame source no longer claims a language. sourceRequest used to answer with mimeType 'text/x-gemstone-smalltalk', which the language declares in package.json, so the document resolved to gemstone-smalltalk and got a gutter. Withholding it costs syntax highlighting in that read-only view -- the debugger panel's own source pane still highlights -- and buys a gutter that never invites a breakpoint it must refuse. The debug adapter still refuses such a request outright, as the backstop for debug.allowBreakpointsEverywhere or a client that is not VS Code. A breakpoint set in a workspace or a .gst file is taken back out of VS Code's list with a message naming where it belongs. The test for that is deliberately narrow: onDidChangeBreakpoints reports every extension's breakpoints, so a rule of "not a gemstone:// URI" would have taken a Python file's breakpoint out of the developer's Breakpoints panel -- a worse bug than the one being fixed. It matches on the language instead, falling back to the file extension for one restored before its editor is open, and leaves everything else alone. Also corrects the comment, README bullet and changelog entry that claimed VS Code would not offer the gutter on a doit frame. It did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correctness, in the order it matters: - A breakpoint is applied to the session its method belongs to, not to whichever session is selected. A method editor stays bound to the session it was opened from, so with two sessions live the selected one is routinely not the gem holding the method: the break landed in the wrong stone, the method on screen never stopped, and the clear that precedes re-arming took out breakpoints the other session had set. Enable/Disable/Remove All sweep every live gem for the same reason, since the rows they flip are one list spanning all sessions, and one gem refusing no longer abandons the rest. A row in the breakpoints view can only match a method from the session it was read from. - A GCI failure while arming is no longer swallowed. The stone's message is shown and carried back in the breakpoint result, and a step point that armed but could not be disabled is cleared rather than left stopping execution behind a marker that reads "disabled". When a method's step points cannot be read after its breaks were cleared, the stale record is dropped so no marker, hover or view row outlives it. - A keyword message's continuation keywords are found past a separator nested in parentheses or a block, so the trailing part of a send keeps its step point number, hover and breakpoint. - A named breakpoint deleted while its class picker was open is no longer resurrected when the picker answers. - The step point numbers toggle claims its new value before writing the setting, so a fast second click flips back instead of being lost. Reuse and cost: - The duplicated step point resolver is gone: Run to Cursor now goes through the shared resolver, so it and a breakpoint set in an editor cannot disagree about which token a step point is. - Method names, error text and the symbol list dictionary lookup all use the existing shared helpers instead of local copies. The shared method-name helper takes just the three parts of a name so every caller can reach it. - Step points are read in one round trip to the stone instead of three, which is extension host time the editor was not drawing in. A stale comment claiming Run to Cursor is unavailable on Executed Code frames is corrected: such a frame is breakable by method OOP. Manually verified against a live stone by Eric: the session a breakpoint is applied to, the view row matching only its own session, Disable All across sessions, the keyword scan, the numbers toggle, and Run to Cursor through the shared resolver. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eric/issue262-breakpoints
Tests first, since three of the six findings were untested claims: - The one-round-trip step point query had never executed against a stone. It replaced three proven queries on the hover and inlay-hint path, and every test mocked it away, so a syntax error or any difference in sourceString / _sourceOffsets between releases would have stopped step point numbers entirely with the suite still green. Three integration cases now compare its three parts against the three queries it replaced, prove the reply is not merely empty by landing a selector range on real source text, and pin the framing with a method whose source carries tabs, blank lines and text shaped like the reply's own header rows. - The debug adapter's relay of a refusal reason was untested on the branch that carries it: a refused breakpoint reports the reason, an accepted one reports neither reason nor message, and the column of an inline breakpoint is forwarded. - A workspace breakpoint is refused whether or not a session is logged in. The refusal deliberately runs before the session lookup, and nothing pinned that. One leak, with the behaviour it could reach: - A method held still while its editor was dirty was only ever released by a revert. A save arrives as a recompile instead, and VS Code fires no text change for it, so the URI stayed held for the life of the window and a later unrelated clean edit would re-apply the method. Released on recompile, and swept when the session logs out. Documentation: - README says a breakpoint belongs to the session its method was opened from, and that Enable/Disable/Remove All sweep every logged-in session — a real behaviour change for anyone running concurrent sessions, previously written down only in the CHANGELOG. - CHANGELOG entries for four fixes that had none: the resurrected named breakpoint, the one-round-trip step point read, the method-name format, and the step point numbers toggle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflicts, both resolved as a union: - CHANGELOG.md — rebuilt from the whole file rather than the conflict hunk, so no section lost bullets that sat outside it. Every bullet from both sides is present; main's Added, Removed and Fixed entries now sit beside the breakpoint work's own. - package.json — main added the GemStone Go Back / Go Forward commands next to the command this branch renamed. Both navigation commands are kept, and the breakpoint command keeps its new name (`gemstone.breakpoints.toggleAtCursor`); no reference to the old `gemstone.toggleSelectorBreakpoint` survives anywhere. `npm install` run afterwards, since main moved esbuild to devDependencies and pinned the @types floor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks @npapagna — all fourteen are addressed, each with a reply in its own thread and the thread resolved. Branch is up to date with Both blockers. The swallowed GCI failure now reports the stone's message and, in the case you spotted, removes a break that armed but could not be disabled rather than leaving it stopping execution behind a marker that reads "disabled". The doit/decompiled blocker went a different way than you asked: the case is now refused rather than threaded through One premise correction, because it changes severity rather than the fix. The three session-scoping findings were called latent on the grounds that Jasper supports only one open session. Concurrent sessions ship today behind Verified by hand against a live stone, by Eric — as distinct from the rest, which rest on automated tests:
Also in this round, from a second pass over the branch after your review:
Every source change was mutation-checked: reverted in turn, with the suite required to go red. Gate is green — lint, formatting, compile, 6,818 client tests, 322 server, 92 mcp, including the live-stone integration tests. |
Closes #262.
Setting a breakpoint meant a gutter click, which lands on the leftmost step point of the line. 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.
The rule everything follows
A GemStone breakpoint lives in the gem. It is not in the repository — no
commitpersists it, verified on 3.6.2 and 3.7.5 — so it cannot outlive the thing it was set in. VS Code's breakpoint list is the working model (which is what makes the gutter, the per-breakpoint checkbox and the built-in Enable/Disable/Remove All controls drive GemStone for free), but deliberately not a durable one. A breakpoint is dropped from that list, not just from the gem, when:Both of those were the opposite way round in the first draft of this branch. They changed because a marker that promises to stop execution it cannot stop is worse than no marker.
What you get
Aiming at a step point. Precision rides on the breakpoint's column: a gutter click carries none and means the leftmost step point on the line, while an inline breakpoint or Toggle Breakpoint at Cursor (
Shift+F9) carries the exact column and picks the step point nearest it. The token that will actually break is outlined — grey and faded when disabled, matching the grey the gutter dot goes.Enable / disable. Per breakpoint from the Breakpoints view checkbox or the cursor commands; Enable All / Disable All / Remove All cover every GemStone breakpoint, including ones Jasper never set. A disabled breakpoint is applied as set-then-disable, because
disableBreakAtStepPoint:is a no-op on a step point with no breakpoint.Step points, numbered but out of the way. Numbers are inlay hints — VS Code's own dim style, suppressed or restyled by its
editor.inlayHints.*settings like any other — each clickable to toggle a breakpoint there.gemstone.stepPoints.displaydefaults todebugging, so they appear while a debug session runs and stay out of the way while you are reading or writing code. Hovering a step point always reports its number and breakpoint state whatever the setting. Numbers are withheld while a buffer has unsaved edits, since the stone's offsets no longer describe the text on screen.A breakpoint manager. A Breakpoints view in the GemStone sidebar lists what the gem actually holds, grouped by class and method with the step point each breakpoint resolved to. It is the gem's truth rather than a copy of VS Code's list, so it also surfaces breakpoints Jasper never set — from topaz, or a
haltleft in the code — which were invisible until execution stopped on one.Break on entry by naming a method. The
+button in VS Code's Breakpoints panel takes a name rather than a location. Type a bare selector and Jasper finds the implementors, asking which class you meant when there is more than one; a qualifiedAccount>>balanceorAccount class>>newskips the prompt but is still checked against the image, since a typo would otherwise set a breakpoint that silently never fires. The name is then replaced by an ordinary breakpoint on the method's first step point, rather than carried as a second kind of breakpoint — so it gains a real location and red dot, enable/disable, and a row in the manager view, and cannot fight a source breakpoint over which of them owns a method's breaks.Everything works in the debugger's source pane too, since the debugger opens ordinary editors.
Unifying two managers that fought each other
The old cursor-toggle command kept its breakpoints in a private map, so they never appeared in the Breakpoints view and were silently wiped whenever a gutter breakpoint on the same method was applied (that path calls
clearAllBreaksfirst). It now adds a normal VS Code breakpoint, so the two kinds are one kind.Bugs found along the way
_allMethodBreakpoints' tuple stride is version-dependent — 3 fields on 3.6.2, 4 on 3.7.5, which gainedbreakpointLevel. Decoding it by hand walks off the end of the array on the older release.getAllBreakpointsreads the kernel's own_breakReport:instead, which knows its own stride. It also skips breakpoints stranded on a superseded method version, which a recompile leaves behind and the gem goes on reporting.gemstone.maxEnvironmentis a ceiling, not a selection, and was passed straight through as the environment to search — so on a stone configured above 0 an ordinary selector came back as "nothing implements it". It now sweeps 0..max and remembers which environment each hit came from.+creates the breakpoint blank and only then opens it for editing, and the typed name arrives as a change, not an addition. Rejecting the blank deleted the row before it could be typed into; watching additions alone never saw the name.Known limitation
VS Code's own Deactivate Breakpoints button greys the breakpoints out but cannot reach GemStone:
vscode.debugexposes no activation state and no event for it, so an extension cannot observe the flag and the breakpoints stay armed in the gem. Disable All Breakpoints is the one that disarms them. Documented in the README.Testing
npm run lint,npm run format:check,npm run compileandnpm testall clean: 6152 client, 322 server, 92 mcp-server.breakpoints.integration.test.tspins the GemStone semantics the design rests on against a live stone, since none of them is documented anywhere we control — the disable-is-a-no-op rule, set-as-enable, out-of-range being silently ignored, breakpoints surviving a transaction abort, and the version-independence of the breakpoint report. Run against both 3.6.2 and 3.7.5.Manually verified end to end in the Extension Development Host across 20 checks: gutter and cursor breakpoints, the panel and the manager view, enable/disable individually and globally, step point numbering and its dirty-buffer suppression, hover, reveal-from-list, the four global toolbar actions, all six named-breakpoint cases, actually stopping on a breakpoint, recompile, logout, and class-side methods.
🤖 Generated with Claude Code