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

## [Unreleased]

### Added

- **A first connect now points you at the basics instead of the raw kernel.** A newly-connected user landed among kernel classes with no signpost to browsing, searching, or a workspace. A **GemStone - Start Here** button now appears in the status bar on connect; clicking it opens a quick pick of *Browse a class*, *Search your code*, *Open a workspace*, and *Take the tour*. It's unobtrusive, so it stays put rather than nagging — dismiss it for good with the quick pick's **Hide the Start Here button** entry (and bring it back with **GemStone: Reset Getting Started**). The same quick pick is always available from the Command Palette as **GemStone: Start Here**. The Get Started walkthrough also gained **Browse classes** and **Search your code** steps, which it previously skipped. ([#468](https://github.com/GemTalk/Jasper/issues/468))
- **A one-time hint explains how to keep multiple methods open in the Explorer.** The Methods pane opens a method in a single reusable preview tab, so single-clicking another method replaces it — a first-time user reads that as the method being lost, and doesn't see how to view two at once. The first time a click is about to replace a previewed method, a one-time toast points out that double-clicking it (or its **Keep Method Open** button) keeps it open while you browse others. The pin button and the method-row tooltip now say the same thing: the button was retitled from **Pin Method** to **Keep Method Open (Pin)**, and the tooltip spells out preview-vs-keep. ([#468](https://github.com/GemTalk/Jasper/issues/468))
- **The breadcrumb over a method source is now a live class navigator.** A `gemstone://` method editor shows a `Dictionary › Class › instance/class › category › selector` breadcrumb above the source, but its dropdowns were empty and clicking a crumb did nothing. Each crumb now drills into the stone: a dictionary lists its classes, a class lists its `instance`/`class` sides plus its `definition`, a side lists its method categories, and a category lists its selectors — so you can reach another method of the same class, or open the class definition, without leaving the editor. ([#468](https://github.com/GemTalk/Jasper/issues/468))
- **GemStone Go Back / Forward buttons retrace the methods you've viewed.** Because methods open in a single reusable preview tab, VS Code's own Go Back couldn't step through them — its history only tracks distinct/pinned tabs, so a first-time user drilling through methods had no way back. Two title-bar buttons on GemStone editors walk a dedicated history of the `gemstone://` editors you've visited and reopen each in the preview tab; visiting a new method drops the forward trail, as a browser does. ([#468](https://github.com/GemTalk/Jasper/issues/468))

### Removed

- **The GemStone Explorer's Open Editors pane is gone; a status-bar button replaces it.** As the topmost pane it appeared the instant you opened your first editor, which reshuffled the sidebar and scrolled your class selection out of view — and it spent a pane's worth of height duplicating what the editor tabs already show. Your open editors are just editor tabs now, and a left status-bar button (**GemStone: Close All GemStone Editors**, also in the Command Palette) tallies them and closes them all in one click. ([#468](https://github.com/GemTalk/Jasper/issues/468))

### Fixed

- **The senders/implementors hover no longer vanishes when one of its queries fails.** Resting on a selector runs `implementorsOf` to list the classes and `sendersOf` for the count; only the senders call was guarded, so a single thrown query — a busy session, or a stone without the browser/RB plugin loaded — rejected the whole hover and silently showed nothing, including the senders count that had already succeeded. The implementors lookup is now guarded the same way, so the hover degrades to what it can show instead of disappearing. ([#432](https://github.com/GemTalk/Jasper/issues/432), [#468](https://github.com/GemTalk/Jasper/issues/468))

## [1.8.13] - 2026-08-20

A follow-up release for **GemStone Search**: correctness fixes for multi-session and multi-environment use, matching and debounce repairs found by a review pass over the feature, one naming pass, and the senders/implementors counts moving off the method source.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ If you already have a GemStone server running on another machine (or locally), y

1. Install the extension from the VS Code Marketplace or Open VSX (links above).
2. Open the **GemStone** sidebar (gem icon in the activity bar).
3. Click the **+** button in the **Logins** section to create a new login.
4. Fill in the connection details: GemStone version, host, stone name, NetLDI, and credentials.
5. Click **Login** to connect.
3. In **Logins & Sessions**, click **Add a Login** to open the login editor.
4. Fill in the connection details, top to bottom: GemStone version, gem host, stone name, NetLDI (service name or port), and your GemStone user/password. **Host User** and **Host Password** are optional — supply them only when the remote NetLDI requires host authentication; leave them blank for a local stone or a guest-mode NetLDI. (Stuck? Click **Help me login** in the login editor for per-field guidance.)
5. Click **Save**, then click the saved login to connect. A "Connecting…" notification reports success or failure, and the status bar (bottom right) shows the active session — or turns red, click-to-explain, if the connection fails.

The first time you log in with a given GemStone version, Jasper needs the native GCI library (`libgcits`) for that version:

Expand Down Expand Up @@ -166,9 +166,9 @@ Long-running expressions show a progress notification with soft-break and hard-b

### GemStone Explorer

The **GemStone Explorer** is the primary way to browse and edit code, and the view to reach for first. It lives in its own activity-bar container as a set of linked panes — **Dictionaries**, **Class Categories**, **Classes**, **Hierarchy**, and **Methods** — plus **Open Editors** for what you have open.
The **GemStone Explorer** is the primary way to browse and edit code, and the view to reach for first. It lives in its own activity-bar container as a set of linked panes — **Dictionaries**, **Class Categories**, **Classes**, **Hierarchy**, and **Methods**. Your open editors appear as ordinary editor tabs; a status-bar button tallies them and closes them all at once (**GemStone: Close All GemStone Editors**).

Selecting down the panes narrows what the next one shows. Click a method to open its source; **Cmd+S** (Ctrl+S) compiles it back to GemStone. Class definitions and comments are editable the same way.
Selecting down the panes narrows what the next one shows. Click a method to open its source; **Cmd+S** (Ctrl+S) compiles it back to GemStone. Class definitions and comments are editable the same way. A single click previews a method in one reusable tab, so clicking another replaces it — double-click a method (or use **Keep Method Open**) to keep it open while you browse others.

Beyond browsing, the Explorer is where the code-changing operations live:

Expand Down
44 changes: 0 additions & 44 deletions client/src/__tests__/explorerOpenEditors.test.ts

This file was deleted.

74 changes: 0 additions & 74 deletions client/src/__tests__/explorerOpenEditorsLabel.test.ts

This file was deleted.

71 changes: 70 additions & 1 deletion client/src/__tests__/explorerOpenMethod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ vi.mock('vscode', () => import('../__mocks__/vscode.js'));
vi.mock('../browserQueries', () => ({}));

import type * as vscode from 'vscode';
import { ExplorerController, MethodItem } from '../gemstoneExplorer';
import { ExplorerController, MethodItem, shouldHintKeepMethodsOpen } from '../gemstoneExplorer';
import { Uri, window, commands, workspace, languages } from '../__mocks__/vscode';
import type { SessionManager, ActiveSession } from '../sessionManager';

Expand All @@ -33,6 +33,26 @@ function makeController(): ExplorerController {
return controllerFor(SESSION);
}

// A controller wired with a fake global-storage memento, backed by `store`, so the
// one-time "keep methods open" hint can be exercised.
function controllerWithGlobalState(store: Record<string, unknown>): ExplorerController {
const sessionManager = { getSelectedSession: () => SESSION } as unknown as SessionManager;
const memento = {
get: (k: string) => store[k],
update: (k: string, v: unknown) => {
store[k] = v;
return Promise.resolve();
},
} as unknown as vscode.Memento;
const ctl = new ExplorerController(sessionManager, undefined, undefined, memento);
ctl.state.dictName = 'UserGlobals';
ctl.state.dictIndex = 1;
ctl.state.className = 'Array';
return ctl;
}

const HINT_KEY = 'gemstone.explorer.keepMethodsOpenHintShown';

function info(over: Partial<SelectorInfo> = {}): SelectorInfo {
return { selector: 'at:', category: 'accessing', overrideBits: 0, sessionBit: 0, ...over };
}
Expand Down Expand Up @@ -156,6 +176,55 @@ describe('ExplorerController.openMethod', () => {
});
});

describe('keep-methods-open hint', () => {
const showInfo = window.showInformationMessage as ReturnType<typeof vi.fn>;

it('fires only when a different method replaces a previewed one, and only once', () => {
expect(shouldHintKeepMethodsOpen(undefined, 'a', false)).toBe(false);
expect(shouldHintKeepMethodsOpen('a', 'a', false)).toBe(false);
expect(shouldHintKeepMethodsOpen('a', 'b', false)).toBe(true);
expect(shouldHintKeepMethodsOpen('a', 'b', true)).toBe(false);
});

it('stays quiet on the first previewed method', async () => {
const ctl = controllerWithGlobalState({});

await ctl.openMethod(methodItem({ selector: 'at:' }), 'preview');

expect(showInfo).not.toHaveBeenCalled();
});

it('explains once when a second, different method replaces the first', async () => {
const store: Record<string, unknown> = {};
const ctl = controllerWithGlobalState(store);

await ctl.openMethod(methodItem({ selector: 'at:' }), 'preview');
await ctl.openMethod(methodItem({ selector: 'size' }), 'preview');
await ctl.openMethod(methodItem({ selector: 'first' }), 'preview');

expect(showInfo).toHaveBeenCalledTimes(1);
expect(store[HINT_KEY]).toBe(true);
});

it('does not fire when it has been shown in a previous session', async () => {
const ctl = controllerWithGlobalState({ [HINT_KEY]: true });

await ctl.openMethod(methodItem({ selector: 'at:' }), 'preview');
await ctl.openMethod(methodItem({ selector: 'size' }), 'preview');

expect(showInfo).not.toHaveBeenCalled();
});

it('does not fire when a pin or keep open replaces the preview (only single-click preview does)', async () => {
const ctl = controllerWithGlobalState({});

await ctl.openMethod(methodItem({ selector: 'at:' }), 'preview');
await ctl.openMethod(methodItem({ selector: 'size' }), 'pin');

expect(showInfo).not.toHaveBeenCalled();
});
});

describe('MethodItem click wiring', () => {
it('routes each click to the double-click hook, carrying the node', () => {
const node = methodItem();
Expand Down
40 changes: 5 additions & 35 deletions client/src/__tests__/explorerViewRegistration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,21 @@ import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';

// Most GemStone Explorer panes are created eagerly with `vscode.window.createTreeView`
// GemStone Explorer panes are created eagerly with `vscode.window.createTreeView`
// (gemstoneExplorer.ts). VS Code registers a contributed view only once its `when`
// clause is satisfied, and createTreeView throws "No view is registered with id:
// <id>" for a view whose `when` is still false. So a createTreeView-backed view
// must NOT be gated on a context key that is false at activation/login.
//
// The Open Editors pane is the exception: it hides when no gemstone:// editor is
// open, so it IS gated on `gemstone.explorerHasOpenEditors` (false at login). That
// is safe ONLY because explorerOpenEditors.ts registers it with
// `registerTreeDataProvider`, which tolerates a hidden view, rather than
// createTreeView. These tests pin that split so neither half regresses (an empty
// pane always showing, or the login crash shipped in 1.8.1 coming back).
// must NOT be gated on a context key that is false at activation/login — that was
// the login crash shipped in 1.8.1. These tests pin that so it can't come back.
const pkgPath = path.resolve(__dirname, '..', '..', '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

const explorerViews: Array<{ id: string; when?: string }> = pkg.contributes.views.gemstoneExplorer;

const OPEN_EDITORS = 'gemstoneExplorerOpenEditors';

describe('GemStone Explorer views are registrable when created', () => {
it('registers an Open Editors pane', () => {
const ids = explorerViews.map((v) => v.id);

expect(ids).toContain(OPEN_EDITORS);
});

it('hides the Open Editors pane when no editor is open (gated on active + content)', () => {
const openEditors = explorerViews.find((v) => v.id === OPEN_EDITORS);

expect(openEditors?.when).toBe('gemstone.explorerActive && gemstone.explorerHasOpenEditors');
it.each(explorerViews)('does not gate $id on a content key that is false at login', (view) => {
expect(view.when ?? '').not.toContain('explorerHasOpenEditors');
});

it('registers the content-gated Open Editors pane with registerTreeDataProvider, not createTreeView', () => {
const src = fs.readFileSync(path.resolve(__dirname, '..', 'explorerOpenEditors.ts'), 'utf-8');

expect(src).toContain('registerTreeDataProvider(VIEW_ID');
expect(src).not.toContain('window.createTreeView(');
});

it.each(explorerViews.filter((v) => v.id !== OPEN_EDITORS))(
'does not gate $id (a createTreeView pane) on a content key that is false at login',
(view) => {
expect(view.when ?? '').not.toContain('explorerHasOpenEditors');
},
);
});

// The Class Hierarchy pane starts collapsed so it doesn't crowd out the other
Expand Down
Loading