llm-slop-detector 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/mandakan/llm-slop-detector/compare/llm-slop-detector-v0.6.2...llm-slop-detector-v0.7.0) (2026-04-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * mark files with slop in the explorer ([#62](https://github.com/mandakan/llm-slop-detector/issues/62)) ([ff4e240](https://github.com/mandakan/llm-slop-detector/commit/ff4e2402a1aee52032061b922214756586cd0b01))
9
+ * publish releases to Open VSX Registry ([#60](https://github.com/mandakan/llm-slop-detector/issues/60)) ([b572f87](https://github.com/mandakan/llm-slop-detector/commit/b572f8770139f773e19f9038ef377016cd64a4a4))
10
+
3
11
  ## [0.6.2](https://github.com/mandakan/llm-slop-detector/compare/llm-slop-detector-v0.6.1...llm-slop-detector-v0.6.2) (2026-04-24)
4
12
 
5
13
 
package/out/extension.js CHANGED
@@ -84,6 +84,112 @@ function isIgnoredDocument(doc) {
84
84
  // Findings keyed by document URI, stashed during scan so the hover provider
85
85
  // can recover rule metadata (pattern, matched char) without rescanning.
86
86
  const FINDINGS_BY_URI = new Map();
87
+ // Explorer decoration provider. Marks files known to contain slop so users
88
+ // can see where work is pending without having to open each file. Populated
89
+ // lazily from two sources: documents scanned while open, and results from
90
+ // the workspace-scan command. Folders get the color via `propagate: true`;
91
+ // VS Code only re-queries ancestors when we fire events for them, so the
92
+ // provider walks up to the workspace root on every state change.
93
+ class SlopDecorationProvider {
94
+ emitter = new vscode.EventEmitter();
95
+ onDidChangeFileDecorations = this.emitter.event;
96
+ slopyPaths = new Set();
97
+ setFileState(uri, hasSlop) {
98
+ if (uri.scheme !== 'file')
99
+ return;
100
+ const p = uri.fsPath;
101
+ const had = this.slopyPaths.has(p);
102
+ if (had === hasSlop)
103
+ return;
104
+ if (hasSlop)
105
+ this.slopyPaths.add(p);
106
+ else
107
+ this.slopyPaths.delete(p);
108
+ this.emitter.fire(this.urisForPathAndAncestors(p));
109
+ }
110
+ applyScanResults(slopyPaths, scannedPaths) {
111
+ const next = new Set(slopyPaths);
112
+ const changed = [];
113
+ for (const p of scannedPaths) {
114
+ const shouldMark = next.has(p);
115
+ const isMarked = this.slopyPaths.has(p);
116
+ if (shouldMark === isMarked)
117
+ continue;
118
+ if (shouldMark)
119
+ this.slopyPaths.add(p);
120
+ else
121
+ this.slopyPaths.delete(p);
122
+ for (const u of this.urisForPathAndAncestors(p))
123
+ changed.push(u);
124
+ }
125
+ if (changed.length > 0)
126
+ this.emitter.fire(changed);
127
+ }
128
+ forgetPath(fsPath) {
129
+ if (!this.slopyPaths.delete(fsPath))
130
+ return;
131
+ this.emitter.fire(this.urisForPathAndAncestors(fsPath));
132
+ }
133
+ clearAll() {
134
+ if (this.slopyPaths.size === 0)
135
+ return;
136
+ this.slopyPaths.clear();
137
+ this.emitter.fire(undefined);
138
+ }
139
+ provideFileDecoration(uri) {
140
+ if (uri.scheme !== 'file')
141
+ return;
142
+ if (this.slopyPaths.has(uri.fsPath)) {
143
+ return {
144
+ badge: 'S',
145
+ tooltip: 'Contains LLM slop',
146
+ color: new vscode.ThemeColor('list.warningForeground'),
147
+ propagate: true,
148
+ };
149
+ }
150
+ // Folders: return decoration if any tracked file lives under this path.
151
+ // The tree rarely has more than a handful of slopy files, so an O(n)
152
+ // scan per folder query is fine and avoids maintaining a separate index.
153
+ const folderPrefix = uri.fsPath + path.sep;
154
+ for (const p of this.slopyPaths) {
155
+ if (p.startsWith(folderPrefix)) {
156
+ return {
157
+ tooltip: 'Contains LLM slop',
158
+ color: new vscode.ThemeColor('list.warningForeground'),
159
+ propagate: true,
160
+ };
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ urisForPathAndAncestors(fsPath) {
166
+ const uris = [vscode.Uri.file(fsPath)];
167
+ const folder = vscode.workspace.workspaceFolders?.find(f => fsPath === f.uri.fsPath || fsPath.startsWith(f.uri.fsPath + path.sep));
168
+ if (!folder)
169
+ return uris;
170
+ let cur = path.dirname(fsPath);
171
+ const root = folder.uri.fsPath;
172
+ while (cur.length >= root.length && cur.startsWith(root)) {
173
+ uris.push(vscode.Uri.file(cur));
174
+ if (cur === root)
175
+ break;
176
+ const parent = path.dirname(cur);
177
+ if (parent === cur)
178
+ break;
179
+ cur = parent;
180
+ }
181
+ return uris;
182
+ }
183
+ }
184
+ let DECORATION_PROVIDER;
185
+ function updateDecorationForDocument(doc, findings) {
186
+ if (!DECORATION_PROVIDER || doc.uri.scheme !== 'file')
187
+ return;
188
+ const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
189
+ if (!cfg.get('decorateExplorer', true))
190
+ return;
191
+ DECORATION_PROVIDER.setFileState(doc.uri, findings.length > 0);
192
+ }
87
193
  const PENDING_REFRESH = new Map();
88
194
  function cancelPendingRefresh(uriKey) {
89
195
  const p = PENDING_REFRESH.get(uriKey);
@@ -217,9 +323,12 @@ function activate(context) {
217
323
  if (!enabled || !SUPPORTED_LANGS.has(doc.languageId) || isIgnoredDocument(doc)) {
218
324
  collection.delete(doc.uri);
219
325
  FINDINGS_BY_URI.delete(doc.uri.toString());
326
+ updateDecorationForDocument(doc, []);
220
327
  return;
221
328
  }
222
- collection.set(doc.uri, scanDocument(doc));
329
+ const diags = scanDocument(doc);
330
+ collection.set(doc.uri, diags);
331
+ updateDecorationForDocument(doc, diags);
223
332
  };
224
333
  // Leading+trailing debounce: first change after idle triggers an immediate
225
334
  // scan so feedback stays snappy; subsequent changes within the window
@@ -297,9 +406,16 @@ function activate(context) {
297
406
  RULES = (0, rules_1.loadRules)(context.extensionUri);
298
407
  reloadIgnore();
299
408
  rebuildSupportedLangs();
409
+ const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
410
+ const decorate = cfg.get('decorateExplorer', true);
411
+ const enabled = cfg.get('enabled', true);
412
+ if (!decorate || !enabled)
413
+ DECORATION_PROVIDER?.clearAll();
300
414
  vscode.workspace.textDocuments.forEach(refresh);
301
415
  updateStatus();
302
416
  };
417
+ DECORATION_PROVIDER = new SlopDecorationProvider();
418
+ context.subscriptions.push(vscode.window.registerFileDecorationProvider(DECORATION_PROVIDER));
303
419
  reloadRules();
304
420
  // Live-reload when a local .llmsloprc.json is created/changed/deleted
305
421
  // anywhere in the workspace. The loader itself only reads the files at
@@ -313,7 +429,13 @@ function activate(context) {
313
429
  collection.delete(doc.uri);
314
430
  FINDINGS_BY_URI.delete(key);
315
431
  updateStatus();
316
- }), vscode.workspace.onDidChangeWorkspaceFolders(reloadRules), vscode.workspace.onDidGrantWorkspaceTrust(reloadRules), vscode.window.onDidChangeActiveTextEditor(() => updateStatus()), vscode.languages.onDidChangeDiagnostics(() => updateStatus()), vscode.workspace.onDidChangeConfiguration(e => {
432
+ }), vscode.workspace.onDidChangeWorkspaceFolders(reloadRules), vscode.workspace.onDidDeleteFiles(e => {
433
+ for (const uri of e.files)
434
+ DECORATION_PROVIDER?.forgetPath(uri.fsPath);
435
+ }), vscode.workspace.onDidRenameFiles(e => {
436
+ for (const { oldUri } of e.files)
437
+ DECORATION_PROVIDER?.forgetPath(oldUri.fsPath);
438
+ }), vscode.workspace.onDidGrantWorkspaceTrust(reloadRules), vscode.window.onDidChangeActiveTextEditor(() => updateStatus()), vscode.languages.onDidChangeDiagnostics(() => updateStatus()), vscode.workspace.onDidChangeConfiguration(e => {
317
439
  if (e.affectsConfiguration('llmSlopDetector'))
318
440
  reloadRules();
319
441
  }), vscode.languages.registerCodeActionsProvider(CODE_ACTION_SELECTORS, new SlopCodeActionProvider(), { providedCodeActionKinds: SlopCodeActionProvider.providedCodeActionKinds }), vscode.languages.registerHoverProvider(CODE_ACTION_SELECTORS, new SlopHoverProvider()), vscode.commands.registerCommand('llmSlopDetector.toggle', async () => {
@@ -450,7 +572,7 @@ async function scanWorkspace() {
450
572
  return;
451
573
  }
452
574
  const extensions = buildScanExtensionMap();
453
- const hits = await vscode.window.withProgress({
575
+ const result = await vscode.window.withProgress({
454
576
  location: vscode.ProgressLocation.Notification,
455
577
  title: 'LLM Slop Detector: scanning workspace',
456
578
  cancellable: true,
@@ -471,8 +593,9 @@ async function scanWorkspace() {
471
593
  }
472
594
  if (token.isCancellationRequested)
473
595
  return undefined;
596
+ const scannedPaths = new Set(targets.map(t => t.absPath));
474
597
  if (targets.length === 0)
475
- return [];
598
+ return { hits: [], scannedPaths };
476
599
  // Use the in-memory text of any open document so unsaved changes are
477
600
  // reflected in the scan, falling back to the on-disk version otherwise.
478
601
  const openText = new Map();
@@ -518,10 +641,17 @@ async function scanWorkspace() {
518
641
  }));
519
642
  if (token.isCancellationRequested)
520
643
  return undefined;
521
- return out;
644
+ return { hits: out, scannedPaths };
522
645
  });
523
- if (hits === undefined)
646
+ if (result === undefined)
524
647
  return;
648
+ const { hits, scannedPaths } = result;
649
+ if (cfg.get('decorateExplorer', true) && DECORATION_PROVIDER) {
650
+ const slopyAbsPaths = new Set();
651
+ for (const h of hits)
652
+ slopyAbsPaths.add(h.uri.fsPath);
653
+ DECORATION_PROVIDER.applyScanResults(slopyAbsPaths, scannedPaths);
654
+ }
525
655
  if (hits.length === 0) {
526
656
  vscode.window.showInformationMessage('LLM Slop Detector: no findings in the workspace.');
527
657
  return;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "llm-slop-detector",
3
3
  "displayName": "LLM Slop Detector",
4
4
  "description": "Highlights invisible Unicode, AI-style punctuation, and telltale LLM phrases in markdown and plain text.",
5
- "version": "0.6.2",
5
+ "version": "0.7.0",
6
6
  "publisher": "thias-se",
7
7
  "engines": {
8
8
  "vscode": "^1.95.0"
@@ -132,6 +132,11 @@
132
132
  "default": [],
133
133
  "description": "File-glob patterns to skip, using .gitignore syntax. Merged with patterns from a .slopignore file at the workspace root. Example: [\"CHANGELOG.md\", \"docs/generated/**\"]. Use !pattern to re-include after a broader ignore."
134
134
  },
135
+ "llmSlopDetector.decorateExplorer": {
136
+ "type": "boolean",
137
+ "default": true,
138
+ "description": "Mark files in the explorer that are known to contain LLM slop. Updates as you open scanned files; run 'Scan workspace' to populate marks for files you haven't opened."
139
+ },
135
140
  "llmSlopDetector.debounceMs": {
136
141
  "type": "number",
137
142
  "default": 150,