Skip to content

Commit 28307a9

Browse files
committed
feat: add 'Run on save' feature to automatically re-run checks on file save
- Introduced a new setting `angularCodeQuality.runOnSave` to enable automatic re-running of relevant tools when files are saved. - Updated the Problems panel in the background without notifications, supporting `.ts`, `.css`, `.scss`, and `package.json` files. - Implemented debounce logic to optimize performance during rapid saves. - Added documentation in CHANGELOG and README for the new feature.
1 parent 5760772 commit 28307a9

6 files changed

Lines changed: 163 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to the Angular Code Quality Toolkit extension are documented in this file.
44

5+
## [Unreleased]
6+
7+
### Added
8+
9+
- **Run on save.** New setting `angularCodeQuality.runOnSave` (default off): when enabled, saving a file re-runs the relevant tool and refreshes the Problems panel in the background — saving a `.ts` file re-runs ESLint and ts-prune, a `.css`/`.scss` file re-runs stylelint, and `package.json` re-runs depcheck. Runs are quiet (no notifications) and debounced, so a "Save All" triggers a single run instead of one per file.
10+
511
## [0.3.1] - 2026-08-23
612

713
Same packaged extension as 0.3.0. Bumped because Open VSX treats versions as immutable — 0.3.0 was published, then deleted, and that identity cannot be reused.

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ To catch unused variables and parameters, add the rule to your ESLint config so
9292
| `angularCodeQuality.depcheck.ignoreAngularImplicit` | `true` | Hide false "unused" hits for packages Angular uses implicitly (`@angular/*`, `zone.js`, `rxjs`, `tslib`, `typescript`, karma/jasmine, builders). |
9393
| `angularCodeQuality.depcheck.ignores` | `[]` | Extra packages to hide (`*` wildcard, e.g. `@my-scope/*`). |
9494
| `angularCodeQuality.revealOutputOnRun` | `false` | Auto-open the Output channel on each run. Off by default — findings go to the Problems panel; enable this only to watch raw tool logs. |
95+
| `angularCodeQuality.runOnSave` | `false` | Re-run the relevant checks automatically when you save a file (see below). |
96+
97+
---
98+
99+
## Run on save
100+
101+
Set `angularCodeQuality.runOnSave` to `true` and the extension re-runs the relevant tool whenever you save — the Problems panel stays current without you triggering **Run all checks** by hand:
102+
103+
| You save… | It re-runs |
104+
| --- | --- |
105+
| a `.ts` file | ESLint + ts-prune |
106+
| a `.css` / `.scss` file | stylelint |
107+
| `package.json` | depcheck |
108+
109+
Runs happen **quietly** in the background (no notifications) and are **debounced**, so a "Save All" or a formatter re-saving triggers a single run rather than one per file. Off by default.
95110

96111
---
97112

‎package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@
177177
"type": "boolean",
178178
"default": false,
179179
"description": "Reveal the Angular Code Quality output channel automatically each time a command runs. Findings appear in the Problems panel; enable this only if you want to watch the raw tool logs."
180+
},
181+
"angularCodeQuality.runOnSave": {
182+
"type": "boolean",
183+
"default": false,
184+
"description": "Automatically re-run the relevant checks when you save a file, updating the Problems panel in the background (no notifications). Saving a .ts file re-runs ESLint and ts-prune; a .css/.scss file re-runs stylelint; package.json re-runs depcheck. Runs are debounced so rapid saves trigger a single run."
180185
}
181186
}
182187
}

‎src/extension.ts‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
defaultProject,
2525
styleGlobsForProject,
2626
} from './angularWorkspace';
27+
import { toolsForSavedFile } from './runOnSave';
2728

2829
const DIAGNOSTIC_SOURCE = 'Angular Code Quality';
2930

@@ -812,6 +813,75 @@ function clearAllDiagnostics(): void {
812813
vscode.window.setStatusBarMessage('Angular Code Quality: cleared all results.', 3000);
813814
}
814815

816+
// --- Run on save ------------------------------------------------------------
817+
818+
/** Coalesce rapid saves (e.g. Save All, formatters re-saving) into one run. */
819+
const RUN_ON_SAVE_DEBOUNCE_MS = 800;
820+
let runOnSaveTimer: ReturnType<typeof setTimeout> | undefined;
821+
const pendingRunOnSaveTools = new Set<ToolKey>();
822+
823+
/** Invoke a single tool's run quietly (no toast/progress) for background refreshes. */
824+
async function runToolByKey(tool: ToolKey): Promise<void> {
825+
const quiet = { quiet: true };
826+
switch (tool) {
827+
case 'depcheck':
828+
await runDepcheck(quiet);
829+
break;
830+
case 'ts-prune':
831+
await runTsPrune(quiet);
832+
break;
833+
case 'eslint':
834+
await runEslint(quiet);
835+
break;
836+
case 'stylelint':
837+
await runStylelint(quiet);
838+
break;
839+
}
840+
}
841+
842+
async function flushRunOnSave(): Promise<void> {
843+
runOnSaveTimer = undefined;
844+
const tools = [...pendingRunOnSaveTools];
845+
pendingRunOnSaveTools.clear();
846+
// Run sequentially so several tools don't contend for the same package manager.
847+
for (const tool of tools) {
848+
await runToolByKey(tool);
849+
}
850+
}
851+
852+
/** Queue the given tools and (re)start the debounce window. */
853+
function scheduleRunOnSave(tools: ToolKey[]): void {
854+
for (const tool of tools) {
855+
pendingRunOnSaveTools.add(tool);
856+
}
857+
if (runOnSaveTimer) {
858+
clearTimeout(runOnSaveTimer);
859+
}
860+
runOnSaveTimer = setTimeout(() => void flushRunOnSave(), RUN_ON_SAVE_DEBOUNCE_MS);
861+
}
862+
863+
function handleDidSave(document: vscode.TextDocument): void {
864+
if (!vscode.workspace.getConfiguration('angularCodeQuality').get<boolean>('runOnSave', false)) {
865+
return;
866+
}
867+
if (document.uri.scheme !== 'file') {
868+
return;
869+
}
870+
const folder = getWorkspaceFolder();
871+
if (!folder) {
872+
return;
873+
}
874+
// Only react to files inside the workspace folder.
875+
const rel = path.relative(folder.uri.fsPath, document.uri.fsPath);
876+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
877+
return;
878+
}
879+
const tools = toolsForSavedFile(document.uri.fsPath);
880+
if (tools.length > 0) {
881+
scheduleRunOnSave(tools);
882+
}
883+
}
884+
815885
export function activate(context: vscode.ExtensionContext): void {
816886
const toolKeys: ToolKey[] = ['depcheck', 'ts-prune', 'eslint', 'stylelint'];
817887
for (const key of toolKeys) {
@@ -838,7 +908,9 @@ export function activate(context: vscode.ExtensionContext): void {
838908
),
839909
vscode.commands.registerCommand('angularCodeQualityToolkit.selectProject', () =>
840910
selectAngularProject()
841-
)
911+
),
912+
// Run-on-save: re-run the relevant tool(s) when a file is saved (opt-in).
913+
vscode.workspace.onDidSaveTextDocument(handleDidSave)
842914
);
843915

844916
// Show the active Angular project in the status bar on startup, if any.
@@ -849,6 +921,11 @@ export function activate(context: vscode.ExtensionContext): void {
849921
}
850922

851923
export function deactivate(): void {
924+
if (runOnSaveTimer) {
925+
clearTimeout(runOnSaveTimer);
926+
runOnSaveTimer = undefined;
927+
}
928+
pendingRunOnSaveTools.clear();
852929
outputChannel?.dispose();
853930
projectStatusBar?.dispose();
854931
projectStatusBar = undefined;

‎src/runOnSave.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as path from 'path';
2+
import { ToolKey } from './diagnostics';
3+
4+
/**
5+
* Maps a saved file to the tools that should re-run for it (run-on-save):
6+
*
7+
* - `package.json` -> depcheck (dependency usage can change)
8+
* - `*.ts` -> ESLint + ts-prune (lint + unused-export analysis)
9+
* - `*.css` / `*.scss` -> stylelint
10+
*
11+
* Returns an empty array for files no tool cares about (e.g. `.html`, `.md`),
12+
* so the caller can cheaply skip scheduling a run.
13+
*
14+
* Pure and vscode-free so it can be unit-tested without the extension host.
15+
*/
16+
export function toolsForSavedFile(filePath: string): ToolKey[] {
17+
const base = path.basename(filePath).toLowerCase();
18+
if (base === 'package.json') {
19+
return ['depcheck'];
20+
}
21+
const ext = path.extname(base);
22+
if (ext === '.ts') {
23+
return ['eslint', 'ts-prune'];
24+
}
25+
if (ext === '.css' || ext === '.scss') {
26+
return ['stylelint'];
27+
}
28+
return [];
29+
}

‎src/test/runOnSave.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { toolsForSavedFile } from '../runOnSave';
4+
5+
test('toolsForSavedFile maps package.json to depcheck', () => {
6+
assert.deepEqual(toolsForSavedFile('/repo/package.json'), ['depcheck']);
7+
// nested package.json (monorepo) still maps by basename
8+
assert.deepEqual(toolsForSavedFile('/repo/apps/web/package.json'), ['depcheck']);
9+
});
10+
11+
test('toolsForSavedFile maps .ts files to eslint + ts-prune', () => {
12+
assert.deepEqual(toolsForSavedFile('/repo/src/app/app.component.ts'), ['eslint', 'ts-prune']);
13+
});
14+
15+
test('toolsForSavedFile maps .css and .scss files to stylelint', () => {
16+
assert.deepEqual(toolsForSavedFile('/repo/src/styles.css'), ['stylelint']);
17+
assert.deepEqual(toolsForSavedFile('/repo/src/app/app.component.scss'), ['stylelint']);
18+
});
19+
20+
test('toolsForSavedFile is case-insensitive on extension and name', () => {
21+
assert.deepEqual(toolsForSavedFile('/repo/Foo.TS'), ['eslint', 'ts-prune']);
22+
assert.deepEqual(toolsForSavedFile('/repo/Styles.SCSS'), ['stylelint']);
23+
assert.deepEqual(toolsForSavedFile('/repo/PACKAGE.JSON'), ['depcheck']);
24+
});
25+
26+
test('toolsForSavedFile returns [] for files no tool handles', () => {
27+
assert.deepEqual(toolsForSavedFile('/repo/src/app/app.component.html'), []);
28+
assert.deepEqual(toolsForSavedFile('/repo/README.md'), []);
29+
assert.deepEqual(toolsForSavedFile('/repo/angular.json'), []);
30+
});

0 commit comments

Comments
 (0)