Skip to content

Commit 69187df

Browse files
committed
Add structured BinarySource enum for ruff/ty resolution
The ruff and ty language servers resolve their binaries through a 3-tier strategy (user-configured path, companion extension, uv install), but previously each server implemented this inline with ad-hoc string labels to identify which source was used. This made it hard to display consistent diagnostics, track resolution paths in telemetry, or test the fallthrough logic in isolation. This introduces a `BinarySource` tagged enum (modeled after `UvBin`) with three variants: `UserConfigured`, `CompanionExtension` (with a `kind` field distinguishing "configured" vs "bundled"), and `UvInstalled`. A generic `resolveBinary()` function tries composable `ResolutionSource` values in order and returns the first match, with structured log annotations on every attempt. ```ts BinarySource.$match(source, { UserConfigured: ({ path }) => ..., CompanionExtension: ({ extensionId, path, kind }) => ..., UvInstalled: ({ path }) => ..., }); ``` The `Running` status for both language servers now carries the full `BinarySource` value, which the health diagnostics format via exhaustive `$match`. A telemetry event type `lsp_binary_resolved` is defined for future instrumentation using `_tag` as the discriminant.
1 parent 6ddea0f commit 69187df

File tree

9 files changed

+532
-137
lines changed

9 files changed

+532
-137
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ extension/.vscode-test/
1010
*.vsix
1111
.vscode-test/
1212
.claude/
13+
CLAUDE.md

extension/src/__mocks__/TestRuffLanguageServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
RuffLanguageServer,
55
RuffLanguageServerStatus,
66
} from "../services/completions/RuffLanguageServer.ts";
7+
import { BinarySource } from "../utils/binaryResolution.ts";
78

89
/**
910
* Test mock for RuffLanguageServer
@@ -22,6 +23,7 @@ export const TestRuffLanguageServerLive = Layer.effect(
2223
Effect.succeed(
2324
RuffLanguageServerStatus.Running({
2425
serverVersion: "0.0.0-test",
26+
binarySource: BinarySource.UvInstalled({ path: "/test/ruff" }),
2527
client: {
2628
start: () => Effect.succeed(Option.none()),
2729
restart: () => Effect.void,

extension/src/__mocks__/TestTyLanguageServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
TyLanguageServer,
55
TyLanguageServerStatus,
66
} from "../services/completions/TyLanguageServer.ts";
7+
import { BinarySource } from "../utils/binaryResolution.ts";
78

89
/**
910
* Test mock for TyLanguageServer.
@@ -27,6 +28,7 @@ export const TestTyLanguageServerLive = Layer.effect(
2728
restart: () => Effect.void,
2829
},
2930
serverVersion: "0.0.0-test",
31+
binarySource: BinarySource.UvInstalled({ path: "/test/ty" }),
3032
pythonEnvironment: Option.none(),
3133
}),
3234
),

extension/src/services/HealthService.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as semver from "@std/semver";
44
import { Effect, Option } from "effect";
55

66
import { MINIMUM_MARIMO_VERSION } from "../constants.ts";
7+
import { BinarySource } from "../utils/binaryResolution.ts";
78
import { getExtensionVersion } from "../utils/getExtensionVersion.ts";
89
import {
910
RuffLanguageServer,
@@ -128,9 +129,10 @@ export class HealthService extends Effect.Service<HealthService>()(
128129
Starting: () => {
129130
lines.push("\tStatus: starting...");
130131
},
131-
Running: ({ serverVersion, pythonEnvironment }) => {
132+
Running: ({ serverVersion, binarySource, pythonEnvironment }) => {
132133
lines.push("\tStatus: running ✓");
133134
lines.push(`\tVersion: ${serverVersion}`);
135+
lines.push(`\tBinary: ${formatBinarySource(binarySource)}`);
134136
if (Option.isSome(pythonEnvironment)) {
135137
const pyPath = pythonEnvironment.value.path;
136138
const pyVersion = pythonEnvironment.value.version
@@ -157,9 +159,10 @@ export class HealthService extends Effect.Service<HealthService>()(
157159
Starting: () => {
158160
lines.push("\tStatus: starting...");
159161
},
160-
Running: ({ serverVersion }) => {
162+
Running: ({ serverVersion, binarySource }) => {
161163
lines.push("\tStatus: running ✓");
162164
lines.push(`\tVersion: ${serverVersion}`);
165+
lines.push(`\tBinary: ${formatBinarySource(binarySource)}`);
163166
},
164167
Failed: ({ message }) => {
165168
lines.push("\tStatus: failed ✗");
@@ -260,3 +263,12 @@ export class HealthService extends Effect.Service<HealthService>()(
260263
}),
261264
},
262265
) {}
266+
267+
function formatBinarySource(source: BinarySource): string {
268+
return BinarySource.$match(source, {
269+
UserConfigured: ({ path }) => `UserConfigured (${path})`,
270+
CompanionExtension: ({ extensionId, path, kind }) =>
271+
`CompanionExtension/${kind} (${extensionId}, ${path})`,
272+
UvInstalled: ({ path }) => `UvInstalled (${path})`,
273+
});
274+
}

extension/src/services/Telemetry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ type EventMap = {
4949
version: string;
5050
};
5151
uv_install_clicked: undefined;
52+
lsp_binary_resolved: {
53+
server: "ruff" | "ty";
54+
source: "UserConfigured" | "CompanionExtension" | "UvInstalled";
55+
kind?: "configured" | "bundled";
56+
version: string;
57+
};
5258
};
5359

5460
/**

extension/src/services/completions/RuffLanguageServer.ts

Lines changed: 56 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import {
1313
import type * as vscode from "vscode";
1414
import * as lsp from "vscode-languageclient/node";
1515

16-
import { validateBinary } from "../../utils/binaryResolution.ts";
16+
import {
17+
BinarySource,
18+
companionExtensionBundledBinary,
19+
companionExtensionConfiguredPath,
20+
resolveBinary,
21+
userConfiguredPath,
22+
} from "../../utils/binaryResolution.ts";
1723
import {
1824
createManagedLanguageClient,
1925
type ManagedLanguageClient,
@@ -23,7 +29,7 @@ import { Config } from "../Config.ts";
2329
import { OutputChannel } from "../OutputChannel.ts";
2430
import { Sentry } from "../Sentry.ts";
2531
import { ExtensionContext } from "../Storage.ts";
26-
import { resolvePlatformBinaryName, Uv } from "../Uv.ts";
32+
import { Uv } from "../Uv.ts";
2733
import { VariablesService } from "../variables/VariablesService.ts";
2834
import { VsCode } from "../VsCode.ts";
2935
import {
@@ -45,6 +51,7 @@ type RuffLanguageServerStatus = Data.TaggedEnum<{
4551
Running: {
4652
readonly client: ManagedLanguageClient;
4753
readonly serverVersion: string;
54+
readonly binarySource: BinarySource;
4855
};
4956
Failed: {
5057
readonly message: string;
@@ -103,7 +110,7 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
103110
}),
104111
);
105112

106-
const binaryPath = yield* resolveRuffBinary();
113+
const resolved = yield* resolveRuffBinary();
107114

108115
// Create isolated sync instance with its own cell count tracking.
109116
const notebookSync = yield* sync.forClient();
@@ -122,7 +129,7 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
122129
});
123130

124131
const installExit = yield* Effect.exit(
125-
createManagedLanguageClient(RUFF_SERVER, binaryPath, {
132+
createManagedLanguageClient(RUFF_SERVER, resolved.path, {
126133
notebookSync,
127134
clientOptions: {
128135
outputChannelName: "marimo (ruff)",
@@ -195,7 +202,11 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
195202

196203
yield* Ref.set(
197204
statusRef,
198-
RuffLanguageServerStatus.Running({ client, serverVersion }),
205+
RuffLanguageServerStatus.Running({
206+
client,
207+
serverVersion,
208+
binarySource: resolved,
209+
}),
199210
);
200211

201212
// Restart the language server when ruff.* settings change
@@ -232,81 +243,59 @@ export class RuffLanguageServer extends Effect.Service<RuffLanguageServer>()(
232243

233244
/**
234245
* Resolves the ruff binary path using a 3-tier strategy:
235-
* 1. User-configured path (marimo.ruff.path)
236-
* 2. Companion extension discovery (charliermarsh.ruff bundled binary or ruff.path setting)
237-
* 3. Fallback to uv installation
246+
* 1. User-configured path (`marimo.ruff.path`)
247+
* 2. Companion extension discovery — first `ruff.path` setting, then bundled binary
248+
* 3. Fallback to `uv pip install`
238249
*/
239250
const resolveRuffBinary = Effect.fn(function* () {
240251
const code = yield* VsCode;
241252
const config = yield* Config;
242253
const uv = yield* Uv;
243254
const context = yield* ExtensionContext;
244255

245-
// Tier 1: User-configured path
246-
const userPath = yield* config.ruff.path;
247-
if (Option.isSome(userPath)) {
248-
const validated = yield* validateBinary(
249-
userPath.value,
250-
RUFF_SERVER.version,
251-
);
252-
if (Option.isSome(validated)) {
253-
yield* Effect.logInfo(
254-
`Using user-configured ruff binary: ${validated.value}`,
255-
);
256-
return validated.value;
257-
}
258-
yield* Effect.logWarning(
259-
`User-configured ruff path "${userPath.value}" is invalid, falling back to discovery`,
260-
);
261-
}
262-
263-
// Tier 2: Companion extension discovery
264256
const ruffExtension = code.extensions.getExtension(RUFF_EXTENSION_ID);
265-
if (Option.isSome(ruffExtension)) {
266-
// First check the companion extension's ruff.path setting
257+
258+
const ruffExtConfiguredPath = Effect.gen(function* () {
267259
const ruffExtConfig = yield* code.workspace.getConfiguration("ruff");
268-
const extConfiguredPath = Option.fromNullable(
269-
ruffExtConfig.get<string>("path"),
270-
).pipe(Option.filter((p) => p.length > 0));
260+
return Option.fromNullable(ruffExtConfig.get<string>("path")).pipe(
261+
Option.filter((p) => p.length > 0),
262+
);
263+
});
271264

272-
if (Option.isSome(extConfiguredPath)) {
273-
const validated = yield* validateBinary(
274-
extConfiguredPath.value,
265+
return yield* resolveBinary(
266+
RUFF_SERVER.name,
267+
[
268+
userConfiguredPath("ruff", RUFF_SERVER.version, config.ruff.path),
269+
companionExtensionConfiguredPath(
270+
"ruff",
275271
RUFF_SERVER.version,
276-
);
277-
if (Option.isSome(validated)) {
278-
yield* Effect.logInfo(
279-
`Using ruff binary from ruff.path setting: ${validated.value}`,
272+
RUFF_EXTENSION_ID,
273+
ruffExtConfiguredPath,
274+
),
275+
companionExtensionBundledBinary(
276+
"ruff",
277+
RUFF_SERVER.version,
278+
RUFF_EXTENSION_ID,
279+
ruffExtension,
280+
),
281+
],
282+
{
283+
label: "uv install",
284+
resolve: Effect.gen(function* () {
285+
const targetPath = NodePath.resolve(
286+
context.globalStorageUri.fsPath,
287+
"libs",
280288
);
281-
return validated.value;
282-
}
283-
}
284-
285-
// Then check bundled binary in the extension's install directory
286-
const bundledPath = NodePath.join(
287-
ruffExtension.value.extensionPath,
288-
"bundled",
289-
"libs",
290-
"bin",
291-
resolvePlatformBinaryName("ruff"),
292-
);
293-
const validated = yield* validateBinary(bundledPath, RUFF_SERVER.version);
294-
if (Option.isSome(validated)) {
295-
yield* Effect.logInfo(
296-
`Using bundled ruff binary from ${RUFF_EXTENSION_ID}: ${validated.value}`,
297-
);
298-
return validated.value;
299-
}
300-
}
301-
302-
// Tier 3: Fallback to uv installation
303-
yield* Effect.logInfo(
304-
"No custom or companion ruff binary found, falling back to uv installation",
289+
const binaryPath = yield* uv.ensureLanguageServerBinaryInstalled(
290+
RUFF_SERVER,
291+
{
292+
targetPath,
293+
},
294+
);
295+
return Option.some(BinarySource.UvInstalled({ path: binaryPath }));
296+
}),
297+
},
305298
);
306-
const targetPath = NodePath.resolve(context.globalStorageUri.fsPath, "libs");
307-
return yield* uv.ensureLanguageServerBinaryInstalled(RUFF_SERVER, {
308-
targetPath,
309-
});
310299
});
311300

312301
/**

0 commit comments

Comments
 (0)