Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<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.
- 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.
Expand Down
1 change: 0 additions & 1 deletion packages/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/e2e/rstest/suite/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 4 additions & 11 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 6 additions & 14 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<stack>.enable && detected(stack)`.
* pass the gate `rstack.<stack>.enable && detected(stack)` after workspace
* trust.
*
* A stack failing to register never takes another stack down.
*/
Expand Down Expand Up @@ -86,7 +87,6 @@ class ExtensionShell {
reasons.add(key);
}
};
note('rstack.enable');
for (const stack of STACK_IDS) {
note(`rstack.${stack}.enable`);
}
Expand Down Expand Up @@ -155,11 +155,11 @@ class ExtensionShell {
}

/**
* `rstack.enable && rstack.<stack>.enable && detected(stack)`.
* `rstack.<stack>.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 {
Expand All @@ -175,14 +175,6 @@ class ExtensionShell {
},
};
}
if (
!vscode.workspace.getConfiguration('rstack').get<boolean>('enable', true)
) {
return {
ok: false,
state: { kind: 'disabled', reason: '`rstack.enable` is off' },
};
}
if (
!vscode.workspace
.getConfiguration(`rstack.${stack}`)
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export interface StackContext {

/**
* One per stack. `register` is called when the stack passes the gate
* (`rstack.enable && rstack.<stack>.enable && detected(stack)`)
* (`rstack.<stack>.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
Expand Down
13 changes: 7 additions & 6 deletions packages/vscode/tests/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});

Expand All @@ -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']);
});
Expand Down