Skip to content

Commit ba50b1b

Browse files
committed
feat(vscode)!: remove the rstack.enable master switch
Per-stack switches remain available for each tool integration. Users can disable the VS Code extension when they need to turn off every integration.
1 parent 395784e commit ba50b1b

8 files changed

Lines changed: 22 additions & 37 deletions

File tree

CONTEXT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Glossary of terms used across rstack-editor. Code, docs, commit messages and rev
77
- **Stack** — one tool integration (lint, test, fmt) hosted by the extension shell. A stack registers against the shell and reports status through it; stacks never own UI chrome.
88
- **Shell** — the always-activating extension core: detection, status bar, output channels, stack lifecycle.
99
- **Detection** — the per-workspace-folder scan deciding which stacks a folder lights up. Detection signals are config files and installed tool binaries, never user settings.
10-
- **Gate** — the per-stack activation condition: detected, workspace trusted, and the enable settings on.
10+
- **Gate** — the per-stack activation condition: detected, workspace trusted, and that stack's enable setting on.
1111

1212
## Runtimes
1313

packages/vscode/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
2727
- **The three tools are treated uniformly by default.** Detection, dependency-change retry, restart semantics, version gating and status reporting follow one shared pattern across the lint/test/fmt stacks; a stack diverges only when its tool forces it, and the divergence is recorded here as a gotcha. When adding behavior to one stack, first ask whether it belongs to all three. This is about behavior, not code — the upstream copies still must not be deduplicated.
2828
- **Not installed is a state, not an error — uniformly.** A folder or project whose dependencies are not installed (no `rstack`, no `@rstest/core`, no `@rslint/core`, a config importing a package that is not there) is the normal state of a fresh clone and of scaffolded templates beside their generator (`create-rstack`'s `template-*`, which declare their own dependencies and are never installed). Every stack reports it the same way: a `disabled` status whose reason names the restart command as the way out (ADR 0002: an install that changes no lockfile fires no detection pass), one `warn` line in the output channel without a stack trace, never a `crashed` status and never a notification. The words come from one place, `shared/notInstalled.ts` (the `formatVersionMismatch` precedent) — each stack keeps its own status machinery, none its own wording; the restart hint is derived from `stackCommandTitle`, which `tests/extension.test.ts` checks against the manifest. Lint's report lives in the `onDocumentFailure` hook (`stacks/lint/index.ts`), which owns the log line too, so the upstream-tracked `RuntimeManager` only defers to it. Rstest classifies the config-import case in the worker (`missingDependencyCauseOf`: Node's `code`, a bare — package-name — specifier, and for a subpath a walk-up proving the package really is absent, so a typo'd relative import or a missing subpath of an installed package stays a real error) because the IPC channel drops the `code` — `NormalizedConfigResult` carries the verdict as data end to end, and `Project` branches on it. The config-import case is implemented for Rstest only today — lint and fmt load configs inside their own servers and cannot classify there yet (#30).
2929
- One stack failing to register or crashing must never take another stack (or the shell) down.
30-
- The shell always activates; per-folder config detection decides which stacks start, and re-runs on config/lockfile changes without a window reload. Enable-settings are coarse kill switches only.
30+
- The shell always activates; per-folder config detection decides which stacks start, and re-runs on config/lockfile changes without a window reload. The per-stack enable settings are coarse kill switches only.
3131
- Reconciles and restarts share one serialized queue (`enqueue`); a reconcile leaves a live stack alone, so the restart path — the commands, and the full pass any relevant settings change triggers — is the only thing that rebuilds one. Do not add a second queue.
3232
- Restart is a shell concern, not a stack one: `rstack.restart` rebuilds every controller, `rstack.<stack>.restart` rebuilds one. A stack must never register its own restart command — a shallower "bounce the tool's process" restart keeps that controller's stale package resolution and version check, which is the bug the command exists to clear.
3333
- A relevant settings change (a gate key, or a key a live controller declares in `restartOnSettings`) triggers **one full restart pass**, never a targeted one. Per-stack selectivity was removed deliberately: settings edits are rare, and deciding per stack between "reconcile handles the gate" and "restart handles the setting" swallowed the restart when one save wrote a gate key at its already-effective value alongside a shared setting. The full pass re-evaluates every gate, so flips in either direction need no special casing.

packages/vscode/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ All settings live under the unified `rstack.*` namespace. There are no `rslint.*
7676

7777
| Setting | Default | Description |
7878
| --- | --- | --- |
79-
| `rstack.enable` | `true` | Master switch for the whole extension. |
8079
| `rstack.nodeExecutable` || Node binary used for the processes that load your project: the lint worker, test worker and `rs fmt` language server. Empty means the extension picks one (`PATH` first, then the `node` your interactive shell resolves). |
8180
| `rstack.rslint.enable` | `true` | Enable/disable the Rslint integration. |
8281
| `rstack.rslint.corePath` || Path to an `@rslint/core` package directory; relative paths resolve from the workspace folder. |

packages/vscode/e2e/rstest/suite/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ suite('Extension Test Suite', () => {
3333
);
3434

3535
// Waits for the `onStartupFinished` activation and the shell registering
36-
// the Rstest stack (the `rstack.enable && rstack.rstest.enable &&
37-
// detected` gate).
36+
// the Rstest stack (the `rstack.rstest.enable && detected` gate after
37+
// workspace trust).
3838
const rstestInstance = await getRstestExports();
3939
const testController = rstestInstance.testController;
4040
assert.ok(

packages/vscode/package.json

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,8 @@
124124
"type": "object",
125125
"title": "Rstack",
126126
"properties": {
127-
"rstack.enable": {
128-
"order": 0,
129-
"type": "boolean",
130-
"default": true,
131-
"scope": "window",
132-
"markdownDescription": "Master switch for the Rstack extension. When disabled, no stack is registered — the status bar item stays visible so the extension can still be told apart from a broken install."
133-
},
134127
"rstack.nodeExecutable": {
135-
"order": 1,
128+
"order": 0,
136129
"type": "string",
137130
"scope": "resource",
138131
"markdownDescription": "Overrides the `node` binary used to spawn processes that load your project — the Rslint worker, the Rstest test worker and the `rs fmt` language server. Provide an absolute path to a Node.js executable (for example, a version-manager or custom build). An explicit choice is always honoured — the escape hatch when no suitable Node.js can be found automatically — but the version check still runs as an advisory: pointing at a Node.js below the supported floor shows a status-bar warning while runs proceed with it. When empty, the extension picks one: the `node` on `PATH` if it is new enough, otherwise the one your interactive shell resolves. Supports the `${workspaceFolder}` placeholder."
@@ -148,7 +141,7 @@
148141
"type": "boolean",
149142
"default": true,
150143
"scope": "window",
151-
"markdownDescription": "Enable the Rslint language server for detected workspace folders. Requires `#rstack.enable#`. This is a window-level kill switch; which folders run Rslint is decided by detection."
144+
"markdownDescription": "Enable the Rslint language server for detected workspace folders. This is a window-level kill switch; which folders run Rslint is decided by detection."
152145
},
153146
"rstack.rslint.corePath": {
154147
"order": 1,
@@ -180,7 +173,7 @@
180173
"type": "boolean",
181174
"default": true,
182175
"scope": "window",
183-
"markdownDescription": "Enable the Rstest test explorer for detected workspace folders. Requires `#rstack.enable#`. This is a window-level kill switch; which folders run Rstest is decided by detection."
176+
"markdownDescription": "Enable the Rstest test explorer for detected workspace folders. This is a window-level kill switch; which folders run Rstest is decided by detection."
184177
},
185178
"rstack.rstest.rstestPackagePath": {
186179
"order": 1,
@@ -318,7 +311,7 @@
318311
"type": "boolean",
319312
"default": true,
320313
"scope": "window",
321-
"markdownDescription": "Enable the `rs fmt` document formatter for detected workspace folders. Requires `#rstack.enable#`. This is a window-level kill switch; which folders are formatted is decided by detection."
314+
"markdownDescription": "Enable the `rs fmt` document formatter for detected workspace folders. This is a window-level kill switch; which folders are formatted is decided by detection."
322315
},
323316
"rstack.fmt.trace.server": {
324317
"order": 1,

packages/vscode/src/extension.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ type Gate =
3737
* The extension shell: it always activates on
3838
* `onStartupFinished` and does exactly three things — create the status bar
3939
* item and the output channels, run detection, and register the stacks that
40-
* pass the gate `rstack.enable && rstack.<stack>.enable && detected(stack)`.
40+
* pass the gate `rstack.<stack>.enable && detected(stack)` after workspace
41+
* trust.
4142
*
4243
* A stack failing to register never takes another stack down.
4344
*/
@@ -86,7 +87,6 @@ class ExtensionShell {
8687
reasons.add(key);
8788
}
8889
};
89-
note('rstack.enable');
9090
for (const stack of STACK_IDS) {
9191
note(`rstack.${stack}.enable`);
9292
}
@@ -155,11 +155,11 @@ class ExtensionShell {
155155
}
156156

157157
/**
158-
* `rstack.enable && rstack.<stack>.enable && detected(stack)`.
158+
* `rstack.<stack>.enable && detected(stack)`, after workspace trust.
159159
*
160-
* The two settings are declared `"scope": "window"` in the manifest, so
161-
* reading them without a resource URI is exactly what they promise: they are
162-
* kill switches for the window. Per-folder granularity is detection's job
160+
* The enable setting is declared `"scope": "window"` in the manifest, so
161+
* reading it without a resource URI is exactly what it promises: it is a
162+
* kill switch for the window. Per-folder granularity is detection's job
163163
* and stays inside the controllers (`foldersFor(stack)`).
164164
*/
165165
private gate(stack: StackId, snapshot: DetectionSnapshot): Gate {
@@ -175,14 +175,6 @@ class ExtensionShell {
175175
},
176176
};
177177
}
178-
if (
179-
!vscode.workspace.getConfiguration('rstack').get<boolean>('enable', true)
180-
) {
181-
return {
182-
ok: false,
183-
state: { kind: 'disabled', reason: '`rstack.enable` is off' },
184-
};
185-
}
186178
if (
187179
!vscode.workspace
188180
.getConfiguration(`rstack.${stack}`)

packages/vscode/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export interface StackContext {
126126

127127
/**
128128
* One per stack. `register` is called when the stack passes the gate
129-
* (`rstack.enable && rstack.<stack>.enable && detected(stack)`)
129+
* (`rstack.<stack>.enable && detected(stack)`, after workspace trust)
130130
* and `dispose` when it stops passing it or the extension deactivates.
131131
*
132132
* `register` rejecting is contained by the shell: the failure is

packages/vscode/tests/extension.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,13 @@ describe('restart-triggering settings', () => {
351351
expect(stacksOf('register')).toContain('rstest');
352352
});
353353

354-
it('runs a full pass for a bare gate write', async () => {
355-
// The gate half of the trigger set, pinned in isolation: the enable keys
356-
// must fire the pass on their own — the other gate tests here also move a
357-
// declared setting, which would fire the pass regardless.
358-
changeSetting('rstack.enable');
354+
it('runs a full pass for a per-stack enable write', async () => {
355+
// The gate half of the trigger set, pinned in isolation: a per-stack
356+
// enable key must fire the pass on its own — the other gate tests here
357+
// also move a declared setting, which would fire the pass regardless.
358+
changeSetting('rstack.fmt.enable');
359359
await settle();
360+
expect(stacksOf('dispose').sort()).toEqual(['fmt', 'rslint', 'rstest']);
360361
expect(stacksOf('register').sort()).toEqual(['fmt', 'rslint', 'rstest']);
361362
});
362363

@@ -368,7 +369,7 @@ describe('restart-triggering settings', () => {
368369
harness.settings.set('rstack.rstest.enable', false);
369370
changeSetting('rstack.rstest.enable');
370371
await settle();
371-
expect(stacksOf('dispose')).toContain('rstest');
372+
expect(stacksOf('dispose').sort()).toEqual(['fmt', 'rslint', 'rstest']);
372373
expect(stacksOf('register')).not.toContain('rstest');
373374
expect(stacksOf('register').sort()).toEqual(['fmt', 'rslint']);
374375
});

0 commit comments

Comments
 (0)