From 2ba37c22cda6b1ac2a0ac827f7acc02dc6be4e87 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Thu, 3 Sep 2026 16:52:16 +0800 Subject: [PATCH] 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. --- CONTEXT.md | 2 +- packages/vscode/AGENTS.md | 2 +- packages/vscode/README.md | 1 - .../vscode/e2e/rstest/suite/index.test.ts | 4 ++-- packages/vscode/package.json | 15 ++++---------- packages/vscode/src/extension.ts | 20 ++++++------------- packages/vscode/src/types.ts | 2 +- packages/vscode/tests/extension.test.ts | 13 ++++++------ 8 files changed, 22 insertions(+), 37 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 8d7480b..98a7b55 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,7 +7,7 @@ Glossary of terms used across rstack-editor. Code, docs, commit messages and rev - **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. - **Shell** — the always-activating extension core: detection, status bar, output channels, stack lifecycle. - **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. -- **Gate** — the per-stack activation condition: detected, workspace trusted, and the enable settings on. +- **Gate** — the per-stack activation condition: detected, workspace trusted, and that stack's enable setting on. ## Runtimes diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 246962c..7b179e4 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -27,7 +27,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - **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. - **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). - One stack failing to register or crashing must never take another stack (or the shell) down. -- 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. +- 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. - 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. - Restart is a shell concern, not a stack one: `rstack.restart` rebuilds every controller, `rstack..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. - 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. diff --git a/packages/vscode/README.md b/packages/vscode/README.md index 11fa3c1..28a99e0 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -76,7 +76,6 @@ All settings live under the unified `rstack.*` namespace. There are no `rslint.* | Setting | Default | Description | | --- | --- | --- | -| `rstack.enable` | `true` | Master switch for the whole extension. | | `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). | | `rstack.rslint.enable` | `true` | Enable/disable the Rslint integration. | | `rstack.rslint.corePath` | — | Path to an `@rslint/core` package directory; relative paths resolve from the workspace folder. | diff --git a/packages/vscode/e2e/rstest/suite/index.test.ts b/packages/vscode/e2e/rstest/suite/index.test.ts index 8e9a021..f37ebed 100644 --- a/packages/vscode/e2e/rstest/suite/index.test.ts +++ b/packages/vscode/e2e/rstest/suite/index.test.ts @@ -33,8 +33,8 @@ suite('Extension Test Suite', () => { ); // Waits for the `onStartupFinished` activation and the shell registering - // the Rstest stack (the `rstack.enable && rstack.rstest.enable && - // detected` gate). + // the Rstest stack (the `rstack.rstest.enable && detected` gate after + // workspace trust). const rstestInstance = await getRstestExports(); const testController = rstestInstance.testController; assert.ok( diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 6cf9238..d7e7e6b 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -124,15 +124,8 @@ "type": "object", "title": "Rstack", "properties": { - "rstack.enable": { - "order": 0, - "type": "boolean", - "default": true, - "scope": "window", - "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." - }, "rstack.nodeExecutable": { - "order": 1, + "order": 0, "type": "string", "scope": "resource", "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 @@ "type": "boolean", "default": true, "scope": "window", - "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." + "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." }, "rstack.rslint.corePath": { "order": 1, @@ -180,7 +173,7 @@ "type": "boolean", "default": true, "scope": "window", - "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." + "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." }, "rstack.rstest.rstestPackagePath": { "order": 1, @@ -318,7 +311,7 @@ "type": "boolean", "default": true, "scope": "window", - "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." + "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." }, "rstack.fmt.trace.server": { "order": 1, diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index a514b7a..e250577 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -37,7 +37,8 @@ type Gate = * The extension shell: it always activates on * `onStartupFinished` and does exactly three things — create the status bar * item and the output channels, run detection, and register the stacks that - * pass the gate `rstack.enable && rstack..enable && detected(stack)`. + * pass the gate `rstack..enable && detected(stack)` after workspace + * trust. * * A stack failing to register never takes another stack down. */ @@ -86,7 +87,6 @@ class ExtensionShell { reasons.add(key); } }; - note('rstack.enable'); for (const stack of STACK_IDS) { note(`rstack.${stack}.enable`); } @@ -155,11 +155,11 @@ class ExtensionShell { } /** - * `rstack.enable && rstack..enable && detected(stack)`. + * `rstack..enable && detected(stack)`, after workspace trust. * - * The two settings are declared `"scope": "window"` in the manifest, so - * reading them without a resource URI is exactly what they promise: they are - * kill switches for the window. Per-folder granularity is detection's job + * The enable setting is declared `"scope": "window"` in the manifest, so + * reading it without a resource URI is exactly what it promises: it is a + * kill switch for the window. Per-folder granularity is detection's job * and stays inside the controllers (`foldersFor(stack)`). */ private gate(stack: StackId, snapshot: DetectionSnapshot): Gate { @@ -175,14 +175,6 @@ class ExtensionShell { }, }; } - if ( - !vscode.workspace.getConfiguration('rstack').get('enable', true) - ) { - return { - ok: false, - state: { kind: 'disabled', reason: '`rstack.enable` is off' }, - }; - } if ( !vscode.workspace .getConfiguration(`rstack.${stack}`) diff --git a/packages/vscode/src/types.ts b/packages/vscode/src/types.ts index bdf97ac..639e3cc 100644 --- a/packages/vscode/src/types.ts +++ b/packages/vscode/src/types.ts @@ -126,7 +126,7 @@ export interface StackContext { /** * One per stack. `register` is called when the stack passes the gate - * (`rstack.enable && rstack..enable && detected(stack)`) + * (`rstack..enable && detected(stack)`, after workspace trust) * and `dispose` when it stops passing it or the extension deactivates. * * `register` rejecting is contained by the shell: the failure is diff --git a/packages/vscode/tests/extension.test.ts b/packages/vscode/tests/extension.test.ts index c1e094d..0d8bf88 100644 --- a/packages/vscode/tests/extension.test.ts +++ b/packages/vscode/tests/extension.test.ts @@ -351,12 +351,13 @@ describe('restart-triggering settings', () => { expect(stacksOf('register')).toContain('rstest'); }); - it('runs a full pass for a bare gate write', async () => { - // The gate half of the trigger set, pinned in isolation: the enable keys - // must fire the pass on their own — the other gate tests here also move a - // declared setting, which would fire the pass regardless. - changeSetting('rstack.enable'); + it('runs a full pass for a per-stack enable write', async () => { + // The gate half of the trigger set, pinned in isolation: a per-stack + // enable key must fire the pass on its own — the other gate tests here + // also move a declared setting, which would fire the pass regardless. + changeSetting('rstack.fmt.enable'); await settle(); + expect(stacksOf('dispose').sort()).toEqual(['fmt', 'rslint', 'rstest']); expect(stacksOf('register').sort()).toEqual(['fmt', 'rslint', 'rstest']); }); @@ -368,7 +369,7 @@ describe('restart-triggering settings', () => { harness.settings.set('rstack.rstest.enable', false); changeSetting('rstack.rstest.enable'); await settle(); - expect(stacksOf('dispose')).toContain('rstest'); + expect(stacksOf('dispose').sort()).toEqual(['fmt', 'rslint', 'rstest']); expect(stacksOf('register')).not.toContain('rstest'); expect(stacksOf('register').sort()).toEqual(['fmt', 'rslint']); });