Skip to content

Commit 71bf6b2

Browse files
committed
Implement initial API
1 parent 17bc028 commit 71bf6b2

File tree

4 files changed

+224
-9
lines changed

4 files changed

+224
-9
lines changed

README.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,86 @@ need to activate the Ruby environment in order to launch Ruby executables, such
77

88
## Features
99

10-
TODO: this extension is getting extracted from the Ruby LSP.
10+
- **Automatic Ruby detection**: Discovers the Ruby interpreter installed on your machine
11+
- **Version manager integrations**: Supports popular version managers including:
12+
- [chruby](https://github.com/postmodern/chruby)
13+
- [rbenv](https://github.com/rbenv/rbenv)
14+
- [rvm](https://rvm.io/)
15+
- [asdf](https://asdf-vm.com/) (with ruby plugin)
16+
- [mise](https://mise.jdx.dev/)
17+
- **Environment activation**: Composes the correct environment variables (`PATH`, `GEM_HOME`, `GEM_PATH`, etc.) to match your shell configuration
18+
- **JIT detection**: Identifies available JIT compilers (YJIT, ZJIT) for the activated Ruby version
19+
- **Shell support**: Works with various shells including bash, zsh, fish, and PowerShell
20+
- **Extension API**: Provides a programmatic API for other extensions to access the activated Ruby environment
1121

1222
## Extension Settings
1323

1424
TODO
1525

1626
## API
1727

18-
TODO
28+
This extension exposes an API that other extensions can use to access the activated Ruby environment.
29+
30+
### Getting the API
31+
32+
```typescript
33+
const rubyEnvExtension = vscode.extensions.getExtension("Shopify.ruby-environments");
34+
35+
if (rubyEnvExtension) {
36+
const api = rubyEnvExtension.exports;
37+
// Use the API...
38+
}
39+
```
40+
41+
### Activating the Ruby Environment
42+
43+
Request the extension to activate Ruby for a specific workspace:
44+
45+
```typescript
46+
api.activate(vscode.workspace.workspaceFolders?.[0]);
47+
```
48+
49+
### Getting the Current Ruby Definition
50+
51+
Retrieve the currently activated Ruby environment:
52+
53+
```typescript
54+
const ruby = api.getRuby();
55+
56+
if (ruby === null) {
57+
console.log("Ruby environment not yet activated");
58+
} else if (ruby.error) {
59+
console.log("Ruby activation failed");
60+
} else {
61+
console.log(`Ruby version: ${ruby.rubyVersion}`);
62+
console.log(`Available JITs: ${ruby.availableJITs.join(", ")}`);
63+
console.log(`GEM_PATH: ${ruby.gemPath.join(":")}`);
64+
}
65+
```
66+
67+
### Subscribing to Ruby Environment Changes
68+
69+
Listen for changes to the Ruby environment (e.g., when the user switches Ruby versions):
70+
71+
```typescript
72+
const disposable = api.onDidRubyChange((event) => {
73+
console.log(`Ruby changed in workspace: ${event.workspace?.name}`);
74+
75+
if (!event.ruby.error) {
76+
console.log(`New Ruby version: ${event.ruby.rubyVersion}`);
77+
}
78+
});
79+
80+
// Add to your extension's subscriptions for automatic cleanup
81+
context.subscriptions.push(disposable);
82+
```
83+
84+
### Extension Dependency
85+
86+
To ensure your extension loads after Ruby Environments, add it as a dependency in your `package.json`:
87+
88+
```json
89+
{
90+
"extensionDependencies": ["Shopify.ruby-environments"]
91+
}
92+
```

src/extension.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,68 @@
11
import * as vscode from "vscode";
22

3-
// The public API that gets exposed to other extensions that depend on Ruby environments
4-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
5-
interface RubyEnvironmentsApi {}
3+
/**
4+
* Represents a Ruby environment that failed to activate
5+
*/
6+
export interface RubyError {
7+
error: true;
8+
}
9+
10+
/**
11+
* JIT compiler types supported by Ruby
12+
*/
13+
export enum JitType {
14+
YJIT = "YJIT",
15+
ZJIT = "ZJIT",
16+
}
17+
18+
/**
19+
* Represents a successfully activated Ruby environment
20+
*/
21+
export interface RubyEnvironment {
22+
error: false;
23+
rubyVersion: string;
24+
availableJITs: JitType[];
25+
env: NodeJS.ProcessEnv;
26+
gemPath: string[];
27+
}
28+
29+
/**
30+
* Represents a Ruby environment definition - either an error or a successful environment
31+
*/
32+
export type RubyDefinition = RubyError | RubyEnvironment;
33+
34+
/**
35+
* Event data emitted when the Ruby environment changes
36+
*/
37+
export interface RubyChangeEvent {
38+
workspace: vscode.WorkspaceFolder | undefined;
39+
ruby: RubyDefinition;
40+
}
41+
42+
/**
43+
* The public API that gets exposed to other extensions that depend on Ruby environments
44+
*/
45+
export interface RubyEnvironmentsApi {
46+
/** Activate the extension for a specific workspace */
47+
activate: (workspace: vscode.WorkspaceFolder | undefined) => Promise<void>;
48+
/** Get the current Ruby definition */
49+
getRuby: () => RubyDefinition | null;
50+
/** Event that fires when the Ruby environment changes */
51+
onDidRubyChange: vscode.Event<RubyChangeEvent>;
52+
}
53+
54+
// Event emitter for Ruby environment changes
55+
const rubyChangeEmitter = new vscode.EventEmitter<RubyChangeEvent>();
56+
57+
export function activate(context: vscode.ExtensionContext): RubyEnvironmentsApi {
58+
// Ensure the event emitter is disposed when the extension is deactivated
59+
context.subscriptions.push(rubyChangeEmitter);
660

7-
export function activate(_context: vscode.ExtensionContext): RubyEnvironmentsApi {
8-
return {};
61+
return {
62+
activate: async (_workspace: vscode.WorkspaceFolder | undefined) => {},
63+
getRuby: () => null,
64+
onDidRubyChange: rubyChangeEmitter.event,
65+
};
966
}
1067

1168
export function deactivate() {}

src/test/extension.test.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,75 @@
11
import * as assert from "assert";
2+
import { suite, test, beforeEach, afterEach } from "mocha";
3+
import { activate, deactivate, RubyEnvironmentsApi } from "../extension";
4+
import { FakeContext, createContext } from "./helpers";
25

36
suite("Extension Test Suite", () => {
4-
test("Sample test", () => {
5-
assert.strictEqual(1 + 1, 2);
7+
suite("activate", () => {
8+
let context: FakeContext;
9+
10+
beforeEach(() => {
11+
context = createContext();
12+
});
13+
14+
afterEach(() => {
15+
context.dispose();
16+
});
17+
18+
test("returns an object implementing RubyEnvironmentsApi", async () => {
19+
const api = activate(context);
20+
21+
assert.strictEqual(typeof api, "object", "activate should return an object");
22+
assert.strictEqual(typeof api.activate, "function", "API should have an activate method");
23+
assert.strictEqual(typeof api.getRuby, "function", "API should have a getRuby method");
24+
assert.strictEqual(typeof api.onDidRubyChange, "function", "API should have an onDidRubyChange event");
25+
26+
const result = api.activate(undefined);
27+
assert.ok(result instanceof Promise, "activate should return a Promise");
28+
await result;
29+
});
30+
31+
test("returned API conforms to RubyEnvironmentsApi interface", () => {
32+
const api = activate(context);
33+
34+
const typedApi: RubyEnvironmentsApi = api;
35+
assert.ok(typedApi, "API should conform to RubyEnvironmentsApi interface");
36+
});
37+
38+
test("getRuby returns null initially", () => {
39+
const api = activate(context);
40+
41+
assert.strictEqual(api.getRuby(), null, "getRuby should return null before activation");
42+
});
43+
44+
test("onDidRubyChange allows subscribing to events", () => {
45+
const api = activate(context);
46+
47+
let eventFired = false;
48+
const disposable = api.onDidRubyChange(() => {
49+
eventFired = true;
50+
});
51+
52+
assert.ok(disposable, "onDidRubyChange should return a disposable");
53+
assert.strictEqual(typeof disposable.dispose, "function", "disposable should have a dispose method");
54+
55+
disposable.dispose();
56+
assert.strictEqual(eventFired, false, "event should not have fired yet");
57+
});
58+
59+
test("adds event emitter to context subscriptions for disposal", () => {
60+
assert.strictEqual(context.subscriptions.length, 0, "subscriptions should be empty initially");
61+
62+
activate(context);
63+
64+
assert.strictEqual(context.subscriptions.length, 1, "should add emitter to subscriptions");
65+
});
66+
});
67+
68+
suite("deactivate", () => {
69+
test("can be called without errors", () => {
70+
assert.doesNotThrow(() => {
71+
deactivate();
72+
}, "deactivate should not throw errors");
73+
});
674
});
775
});

src/test/helpers.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as vscode from "vscode";
2+
3+
export type FakeContext = vscode.ExtensionContext & { dispose: () => void };
4+
5+
export function createContext(): FakeContext {
6+
const subscriptions: vscode.Disposable[] = [];
7+
8+
return {
9+
subscriptions,
10+
dispose: () => {
11+
subscriptions.forEach((subscription) => {
12+
subscription.dispose();
13+
});
14+
},
15+
} as unknown as FakeContext;
16+
}

0 commit comments

Comments
 (0)