Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c413425
Breakpoints: aim at a step point, enable/disable, and a manager view
ericwinger Aug 22, 2026
99a414c
Say why a breakpoint command did nothing
ericwinger Aug 24, 2026
ee0f1df
Re-apply breakpoints when a session is selected
ericwinger Aug 24, 2026
8cc0038
Breakpoints die with their GemStone session
ericwinger Aug 24, 2026
dc1bfde
Make a disabled breakpoint's token marker legible
ericwinger Aug 24, 2026
1239f7b
Reach the step-point toggle without leaving the method
ericwinger Aug 24, 2026
34c9d0b
Warn that a function breakpoint will never fire
ericwinger Aug 24, 2026
9ea7cff
Break on entry by naming a method in the Breakpoints panel
ericwinger Aug 24, 2026
66ca961
Surface a function-breakpoint resolution that fails
ericwinger Aug 24, 2026
94d20fc
Let the developer finish typing a function breakpoint's name
ericwinger Aug 24, 2026
f81aafc
Document that VS Code's Deactivate Breakpoints cannot reach GemStone
ericwinger Aug 24, 2026
ab269a2
Search every method environment for a named breakpoint's implementors
ericwinger Aug 24, 2026
61f3617
Recompiling a method clears its breakpoints
ericwinger Aug 24, 2026
e571af7
Bring the changelog in line with what shipped
ericwinger Aug 24, 2026
00770e0
Merge remote-tracking branch 'origin/main' into eric/issue262-breakpo…
ericwinger Aug 24, 2026
38c1dc9
Warn that a breakpoint condition will not be honoured
ericwinger Aug 24, 2026
f915811
Merge main into the breakpoints branch
ericwinger Aug 25, 2026
d7a9d95
Merge main into the breakpoints branch
ericwinger Aug 25, 2026
5335fea
Merge main into the breakpoints branch
ericwinger Aug 25, 2026
f992291
Merge the 1.8.14 release into the breakpoints branch
ericwinger Aug 26, 2026
b995987
Merge main into the breakpoints branch
ericwinger Aug 26, 2026
88b2995
Merge branch 'main' into eric/issue262-breakpoints
ericwinger Aug 27, 2026
746162f
Set breakpoints only where the editor is a compiled method's source
ericwinger Aug 27, 2026
30c86eb
Stop offering the breakpoint gutter where a breakpoint cannot be hono…
ericwinger Aug 27, 2026
5e3d0c7
Address review on the breakpoints work
ericwinger Aug 27, 2026
69ddb61
Merge remote-tracking branch 'origin/eric/issue262-breakpoints' into …
ericwinger Aug 27, 2026
20f30fd
Close the gaps a second review found in the breakpoint work
ericwinger Aug 27, 2026
70deef2
Merge main into the breakpoints branch
ericwinger Aug 27, 2026
52af54f
Merge remote-tracking branch 'origin/main' into eric/issue262-breakpo…
ericwinger Aug 27, 2026
74696b0
Merge remote-tracking branch 'origin/main' into eric/issue262-breakpo…
ericwinger Aug 27, 2026
ed1d00b
Merge branch 'main' into eric/issue262-breakpoints
ericwinger Aug 31, 2026
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
25 changes: 25 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

99 changes: 97 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ The only difference is cardinality: a login may now have several session childre

> **Note:** In multiple-session mode, an open workspace/editor stays bound to the session that opened it even after you switch the active session, so the active session, the Explorer, and an open editor can point at different sessions at once. If you use a custom `gemstone.exportPath`, include the `{session}` variable so concurrent sessions don't overwrite each other's exported files.

> **Breakpoints follow the editor, not the active session.** Because an editor stays bound to the session that opened it, a breakpoint set in that editor is armed in that session's gem — so it stops the code you are looking at rather than the session that happens to be selected. **Enable All**, **Disable All** and **Remove All Breakpoints** go the other way and sweep *every* logged-in session, because VS Code keeps a single breakpoint list for the whole window. See [Breakpoints](#breakpoints).

### Code Execution

With an active session, execute Smalltalk code from any editor:
Expand Down Expand Up @@ -256,8 +258,101 @@ When code execution hits an error, a **Debug** button opens the VS Code debugger

### Breakpoints

- **Line breakpoints** — click the editor gutter in a `gemstone://` method to set/clear breakpoints mapped to GemStone step points
- **Selector breakpoints** — right-click a selector and choose **Toggle Selector Breakpoint** to break whenever that selector is sent; breakpointed selectors are highlighted with a red border
Breakpoints live in VS Code's own breakpoint list, so the familiar gutter,
checkbox and Enable/Disable/Remove All controls all drive GemStone. Each one is
applied to the session as a step-point break.

**A GemStone breakpoint is tied to the compiled method it was set in.** It lives
in the gem, not the repository — no `commit` persists it — so it goes away when
the thing it was set in goes away:

- **logging out** clears it, from VS Code's list as well as the gem, so no marker
is left for something that no longer exists. Unlike a breakpoint on a file, it
does not come back when you reopen the window;
- **recompiling the method** clears it too. The recompile replaces the method, and
after an edit the same step point may be a different expression — so the
breakpoint is dropped rather than quietly moved somewhere you didn't put it.

- **Only in a method editor.** A breakpoint names a step point in a method that
lives in the gem, so a `gemstone://` method editor with no unsaved edits is
the only place one can be set. Not a workspace, not a `.gst` file, and not an
**Executed Code** (doit) frame in the call stack — a doit's method is compiled
for that one execution and gone afterwards, so a breakpoint on it could never
be hit again. VS Code offers its gutter per *language*, which is the same for
all four, so a breakpoint set in the wrong one is taken back out with a message
saying where it belongs
- **Unsaved edits hold a method's breakpoints as they are.** Step point numbers
come from the compiled method, and VS Code moves its breakpoints as you type,
so while an editor is dirty the two describe different code. No new breakpoint
is accepted — it is taken back out of the list, with a message naming the two
ways on: save the method, or **File: Revert File** to drop the edits. What is
already set is left alone rather than re-applied against positions that have
moved, so it is still there, on the same step points, once the editor is clean
- **Line breakpoints** — click the editor gutter in a `gemstone://` method. A
gutter click means "this line", and lands on the leftmost step point on it
- **Step-point breakpoints** — a Smalltalk line usually holds several step
points. **Toggle Breakpoint at Cursor** (`Shift+F9`) breaks at the step point
under the caret, not the first one on the line, and the token that will
actually break is outlined
- **Enable / disable** — per breakpoint from the Breakpoints view checkbox or
**Enable/Disable Breakpoint at Cursor**. A disabled breakpoint stays set in the
gem so re-arming it is instant; its token marker turns grey and faded, matching
the grey the gutter dot goes
- **A breakpoint belongs to the session its method came from.** A method editor
stays bound to the session that opened it, so a breakpoint set in it is armed
in *that* session's gem — not in whichever session happens to be active. With
one session live there is no difference; with several, this is what makes the
breakpoint stop the code you were looking at. A row in the **GemStone
Breakpoints** view can likewise only act on a method from the session the view
is reading
- **All at once** — **Enable All**, **Disable All** and **Remove All
Breakpoints** act on every GemStone breakpoint in **every logged-in session**,
including any set outside Jasper by topaz or a `halt` left in the code. VS Code
keeps one breakpoint list for the window, so "all" means all of it: sweeping
only the active session would leave rows reading *disabled* over breakpoints
that still stop execution. If one session's gem refuses, the others are still
swept and the message names the one that failed
- **Clear All Breakpoints in Method** drops every breakpoint in the method you
are in
- **Not honoured: conditions, hit counts and log messages.** VS Code's *Edit
Breakpoint* accepts all three; GemStone breakpoints stop every time the step
point is reached, so Jasper warns when you set one rather than quietly ignoring
it. Conditional breakpoints are tracked under
[#277](https://github.com/GemTalk/Jasper/issues/277)
- **Avoid VS Code's own "Deactivate Breakpoints"** button (the filled-dot icon in
the Breakpoints panel header). It greys the breakpoints out in the panel, but
the VS Code API exposes no way for an extension to observe that state — so
Jasper never hears about it and GemStone keeps stopping on them. Use
**Disable All Breakpoints** instead, which disarms them in the gem
- **Break on entry by name** — the `+` button in VS Code's Breakpoints panel
takes a method name instead of a location. Type a selector (`balance`) and
Jasper finds the implementors, asking which class you meant when there is more
than one; or qualify it yourself (`Account>>balance`, `Account class>>new`).
The name is then replaced by an ordinary breakpoint on the method's first step
point, so it gets a real location, a red dot, and everything else breakpoints
do — handy for stopping in a method without going to find it first

#### Step points

- **Numbered inline** — step point numbers are drawn as inlay hints, and each is
clickable to toggle a breakpoint there. `gemstone.stepPoints.display` controls
when: `debugging` (the default — visible while a debug session runs, out of the
way otherwise), `always`, or `off`. **Toggle Step Point Numbers** flips them
without leaving the editor — it's the `123` button in the editor title bar, and
on the right-click menu
- **On hover** — hovering a step point always reports its number and breakpoint
state, with links to set, clear, enable or disable it, whatever the numbering
is set to
- Numbers are suppressed while a buffer has unsaved edits, since the stone's
offsets no longer line up with what you are looking at

#### Breakpoint manager

The **Breakpoints** view in the GemStone sidebar lists what the current session's
gem actually holds, grouped by class and method with the step point each
breakpoint resolved to. It shows breakpoints Jasper never set, which are
otherwise invisible until execution stops on one. Rows carry an enable checkbox;
clicking one opens the method with the caret on the step point.

### SUnit Test Runner

Expand Down
85 changes: 82 additions & 3 deletions client/src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class TreeItem {
contextValue?: string;
command?: unknown;
collapsibleState?: number;
checkboxState?: number;

constructor(label: string, collapsibleState?: number) {
this.label = label;
Expand All @@ -75,6 +76,11 @@ export const TreeItemCollapsibleState = {
Expanded: 2,
};

export const TreeItemCheckboxState = {
Unchecked: 0,
Checked: 1,
};

// ── ThemeIcon mock ─────────────────────────────────────────

export class ThemeIcon {
Expand Down Expand Up @@ -313,6 +319,8 @@ export const window = {
showWarningMessage: vi.fn(),
createTreeView: vi.fn(() => ({
onDidChangeVisibility: new EventEmitter<{ visible: boolean }>().event,
onDidChangeCheckboxState: new EventEmitter<{ items: [unknown, number][] }>().event,
reveal: vi.fn(),
dispose: () => {},
})),
registerFileDecorationProvider: vi.fn(() => ({ dispose: () => {} })),
Expand Down Expand Up @@ -663,10 +671,18 @@ export class Selection extends Range {
}

export class Location {
public readonly range: Range;
// The real API normalizes a Position into an empty Range, and callers rely on
// `location.range.start` always being there — so the mock must too.
constructor(
public readonly uri: Uri,
public readonly range: Position | Range,
) {}
rangeOrPosition: Position | Range,
) {
this.range =
rangeOrPosition instanceof Range
? rangeOrPosition
: new Range(rangeOrPosition, rangeOrPosition);
}
}

export class CodeActionKind {
Expand Down Expand Up @@ -747,6 +763,7 @@ export const languages = {
registerHoverProvider: vi.fn(() => ({ dispose: () => {} })),
registerCompletionItemProvider: vi.fn(() => ({ dispose: () => {} })),
registerCodeLensProvider: vi.fn(() => ({ dispose: () => {} })),
registerInlayHintsProvider: vi.fn(() => ({ dispose: () => {} })),
setTextDocumentLanguage: vi.fn(),
createDiagnosticCollection: vi.fn((_name?: string) => createMockDiagnosticCollection()),
getDiagnostics: vi.fn((_uri?: unknown) => [] as Diagnostic[]),
Expand Down Expand Up @@ -835,7 +852,28 @@ export const CompletionItemKind = {

export const debug = {
breakpoints: [] as unknown[],
onDidChangeBreakpoints: vi.fn(() => ({ dispose: () => {} })),
activeDebugSession: undefined as unknown,
// Declares its listener parameter so a test can recover the registered handler
// from mock.calls and drive the manager the way VS Code does.
onDidChangeBreakpoints: vi.fn(
(
_listener: (e: {
added: readonly unknown[];
removed: readonly unknown[];
changed: readonly unknown[];
}) => void,
) => ({ dispose: () => {} }),
),
onDidStartDebugSession: vi.fn(() => ({ dispose: () => {} })),
onDidTerminateDebugSession: vi.fn(() => ({ dispose: () => {} })),
// Mirror the real API's side effect on `debug.breakpoints`, so a test can
// drive the manager the way VS Code does and then read the list back.
addBreakpoints: vi.fn((bps: unknown[]) => {
debug.breakpoints = [...debug.breakpoints, ...bps];
}),
removeBreakpoints: vi.fn((bps: unknown[]) => {
debug.breakpoints = debug.breakpoints.filter((bp) => !bps.includes(bp));
}),
startDebugging: vi.fn(),
registerDebugAdapterDescriptorFactory: vi.fn(() => ({ dispose: () => {} })),
registerDebugConfigurationProvider: vi.fn(() => ({ dispose: () => {} })),
Expand All @@ -853,12 +891,53 @@ export class SourceBreakpoint extends Breakpoint {
constructor(
public location: Location,
enabled = true,
condition?: string,
hitCondition?: string,
logMessage?: string,
) {
super();
this.enabled = enabled;
this.condition = condition;
this.hitCondition = hitCondition;
this.logMessage = logMessage;
}
}

// A breakpoint named rather than located — what the Breakpoints panel's `+`
// button creates. Jasper only implements SourceBreakpoint, so it warns on these.
export class FunctionBreakpoint extends Breakpoint {
constructor(
public functionName: string,
enabled = true,
) {
super();
this.enabled = enabled;
}
}

// ── Inlay hint mock ──────────────────────────────────────

export const InlayHintKind = {
Type: 1,
Parameter: 2,
};

export class InlayHintLabelPart {
tooltip?: unknown;
command?: unknown;
constructor(public value: string) {}
}

export class InlayHint {
paddingLeft?: boolean;
paddingRight?: boolean;
constructor(
public position: Position,
public label: string | InlayHintLabelPart[],
public kind?: number,
) {}
}

// ── Test API mock ────────────────────────────────────────

export class TestMessage {
Expand Down
Loading