Skip to content

Commit b359a60

Browse files
committed
feat: add support for optional plugin configuration and related quick fixes
1 parent 2aa8c5a commit b359a60

9 files changed

Lines changed: 449 additions & 18 deletions

AGENTS.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,15 @@ src/
126126
## Build & Test
127127

128128
```bash
129-
npm run compile # Build extension
130-
npm test # Run all tests
131-
npm run lint # Check for issues
132-
npm run fix # Auto-fix lint + format
129+
npm run compile # Build extension
130+
npm run compile-tests # Build tests (required after editing test files)
131+
npm test # Run all tests
132+
npm run lint # Check for issues
133+
npm run fix # Auto-fix lint + format
133134
```
134135

136+
> **Note**: When editing test files, run `compile-tests` before `npm test`, or use the `tasks: watch-tests` VS Code task to auto-compile both extension and tests.
137+
135138
## Dependencies
136139

137140
- **json-to-ast**: Parsing JSON for diagnostics (preserves locations)

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Snippets: Added `devproxy-plugin-mock-stdio-response-file-schema` - MockStdioResponsePlugin mocks file schema
1919
- Diagnostics: Added clickable diagnostic codes that link to documentation
2020
- Diagnostics: Added `emptyUrlsToWatch` warning when urlsToWatch array is empty
21+
- Diagnostics: Added `pluginConfigOptional` info when plugin can be configured with optional config section
22+
- Quick Fixes: Added fix to add optional plugin configuration (adds configSection + config)
23+
- Quick Fixes: Added fix to add missing config section when referenced but not defined
2124

2225
### Changed:
2326

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ Real-time validation of your configuration files. Click any diagnostic code to v
183183
| `emptyUrlsToWatch` | No URLs configured to intercept |
184184
| `pluginConfigRequired` | Plugin requires a config section |
185185
| `pluginConfigMissing` | Referenced config section doesn't exist |
186+
| `pluginConfigOptional` | Plugin can be configured (optional) |
186187
| `pluginConfigNotRequired` | Plugin doesn't support configuration |
187188

188189
### Quick Fixes
@@ -192,6 +193,8 @@ One-click fixes for common issues:
192193
- **Update schema** - Match schema to installed Dev Proxy version
193194
- **Update plugin path** - Fix deprecated `dev-proxy-plugins.dll` paths (single or all at once)
194195
- **Add languageModel configuration** - Enable language model for AI plugins
196+
- **Add plugin configuration** - Add optional config section for plugins that support it
197+
- **Add missing config section** - Create config section when plugin references one that doesn't exist
195198

196199
### Code Lens
197200

src/code-actions.ts

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as vscode from 'vscode';
2-
import {DevProxyInstall} from './types';
2+
import { DevProxyInstall } from './types';
33
import parse from 'json-to-ast';
44
import { getASTNode, getRangeFromASTNode } from './utils';
5+
import { pluginSnippets } from './data';
6+
import snippetsJson from './snippets/json-snippets.json';
57

68
/**
79
* Extract the diagnostic code value from the object format.
@@ -68,6 +70,8 @@ export const registerCodeActions = (context: vscode.ExtensionContext) => {
6870
registerInvalidSchemaFixes(devProxyVersion, context);
6971
registerDeprecatedPluginPathFixes(context);
7072
registerLanguageModelFixes(context);
73+
registerOptionalConfigFixes(context);
74+
registerMissingConfigFixes(context);
7175
};
7276

7377
function registerInvalidSchemaFixes(
@@ -259,3 +263,234 @@ function registerLanguageModelFixes(context: vscode.ExtensionContext) {
259263

260264
registerJsonCodeActionProvider(context, languageModelMissing);
261265
}
266+
267+
function registerOptionalConfigFixes(context: vscode.ExtensionContext) {
268+
const optionalConfig: vscode.CodeActionProvider = {
269+
provideCodeActions: (document, range, context) => {
270+
const currentDiagnostic = findDiagnosticByCode(
271+
context.diagnostics,
272+
'pluginConfigOptional',
273+
range
274+
);
275+
276+
if (!currentDiagnostic) {
277+
return [];
278+
}
279+
280+
// Extract plugin name from diagnostic message
281+
const match = currentDiagnostic.message.match(/^(\w+) can be configured/);
282+
if (!match) {
283+
return [];
284+
}
285+
286+
const pluginName = match[1];
287+
const pluginSnippet = pluginSnippets[pluginName];
288+
289+
if (!pluginSnippet?.config) {
290+
return [];
291+
}
292+
293+
const configSnippetName = pluginSnippet.config.name;
294+
const snippets = snippetsJson as Record<
295+
string,
296+
{ prefix: string; body: string[]; description: string }
297+
>;
298+
299+
// Find the config snippet by matching the prefix
300+
const configSnippet = Object.values(snippets).find(
301+
s => s.prefix === configSnippetName
302+
);
303+
304+
if (!configSnippet) {
305+
return [];
306+
}
307+
308+
const fix = new vscode.CodeAction(
309+
`Add ${pluginName} configuration`,
310+
vscode.CodeActionKind.QuickFix
311+
);
312+
313+
fix.edit = new vscode.WorkspaceEdit();
314+
315+
try {
316+
const documentNode = parse(document.getText()) as parse.ObjectNode;
317+
const pluginsNode = getASTNode(
318+
documentNode.children,
319+
'Identifier',
320+
'plugins'
321+
);
322+
323+
if (!pluginsNode || pluginsNode.value.type !== 'Array') {
324+
return [];
325+
}
326+
327+
// Find the plugin node that matches the diagnostic range
328+
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
329+
.children as parse.ObjectNode[];
330+
331+
const targetPlugin = pluginNodes.find(pluginNode => {
332+
const nameNode = getASTNode(pluginNode.children, 'Identifier', 'name');
333+
if (!nameNode) return false;
334+
const nodeRange = getRangeFromASTNode(nameNode.value);
335+
return nodeRange.intersection(currentDiagnostic.range);
336+
});
337+
338+
if (!targetPlugin) {
339+
return [];
340+
}
341+
342+
// Extract config section name from the snippet body (e.g., "cachingGuidance": { -> cachingGuidance)
343+
const configSectionMatch = configSnippet.body[0].match(/"(\w+)":/);
344+
if (!configSectionMatch) {
345+
return [];
346+
}
347+
348+
const configSectionName = configSectionMatch[1];
349+
350+
// 1. Add configSection property to the plugin
351+
// Find the last property in the plugin to add after it
352+
const lastPluginProperty =
353+
targetPlugin.children[targetPlugin.children.length - 1];
354+
const insertConfigSectionPos = new vscode.Position(
355+
lastPluginProperty.loc!.end.line - 1,
356+
lastPluginProperty.loc!.end.column
357+
);
358+
359+
fix.edit.insert(
360+
document.uri,
361+
insertConfigSectionPos,
362+
`,\n "configSection": "${configSectionName}"`
363+
);
364+
365+
// 2. Add the config section at the root level
366+
// Find the last property in the document
367+
const lastDocProperty =
368+
documentNode.children[documentNode.children.length - 1];
369+
const insertConfigPos = new vscode.Position(
370+
lastDocProperty.loc!.end.line - 1,
371+
lastDocProperty.loc!.end.column
372+
);
373+
374+
// Build the config section from snippet body
375+
// Remove tabstops ($1, $2) and unescape special characters (\$ -> $, \" -> ")
376+
const configBody = configSnippet.body
377+
.map(line =>
378+
line
379+
.replace(/\$\d+/g, '')
380+
.replace(/\\"/g, '"')
381+
.replace(/\\\$/g, '$')
382+
)
383+
.join('\n ');
384+
385+
fix.edit.insert(document.uri, insertConfigPos, ',\n ' + configBody);
386+
387+
// Format the document after the edit is applied
388+
fix.command = {
389+
command: 'editor.action.formatDocument',
390+
title: 'Format Document',
391+
};
392+
} catch {
393+
return [];
394+
}
395+
396+
fix.isPreferred = true;
397+
return [fix];
398+
},
399+
};
400+
401+
registerJsonCodeActionProvider(context, optionalConfig);
402+
}
403+
404+
/**
405+
* Registers code actions to add missing config sections.
406+
* Triggered when a plugin has a configSection property but the config section doesn't exist.
407+
*/
408+
export function registerMissingConfigFixes(
409+
context: vscode.ExtensionContext
410+
): void {
411+
const missingConfig: vscode.CodeActionProvider = {
412+
provideCodeActions: (document, range, context) => {
413+
const currentDiagnostic = findDiagnosticByCode(
414+
context.diagnostics,
415+
'pluginConfigMissing',
416+
range
417+
);
418+
419+
if (!currentDiagnostic) {
420+
return [];
421+
}
422+
423+
// Extract config section name from diagnostic message
424+
// Message format: "configSectionName config section is missing. Use 'snippet-name' snippet to create one."
425+
const match = currentDiagnostic.message.match(
426+
/^(\w+) config section is missing\. Use '([^']+)' snippet/
427+
);
428+
if (!match) {
429+
return [];
430+
}
431+
432+
const configSectionName = match[1];
433+
const configSnippetName = match[2];
434+
435+
const snippets = snippetsJson as Record<
436+
string,
437+
{ prefix: string; body: string[]; description: string }
438+
>;
439+
440+
// Find the config snippet by matching the prefix
441+
const configSnippet = Object.values(snippets).find(
442+
s => s.prefix === configSnippetName
443+
);
444+
445+
if (!configSnippet) {
446+
return [];
447+
}
448+
449+
const fix = new vscode.CodeAction(
450+
`Add ${configSectionName} config section`,
451+
vscode.CodeActionKind.QuickFix
452+
);
453+
454+
fix.edit = new vscode.WorkspaceEdit();
455+
456+
try {
457+
const documentNode = parse(document.getText()) as parse.ObjectNode;
458+
459+
// Add the config section at the root level
460+
// Find the last property in the document
461+
const lastDocProperty =
462+
documentNode.children[documentNode.children.length - 1];
463+
const insertConfigPos = new vscode.Position(
464+
lastDocProperty.loc!.end.line - 1,
465+
lastDocProperty.loc!.end.column
466+
);
467+
468+
// Build the config section from snippet body
469+
// Remove tabstops ($1, $2) and unescape special characters (\$ -> $, \" -> ")
470+
const configBody = configSnippet.body
471+
.map(line =>
472+
line
473+
.replace(/\$\d+/g, '')
474+
.replace(/\\"/g, '"')
475+
.replace(/\\\$/g, '$')
476+
)
477+
.join('\n ');
478+
479+
fix.edit.insert(document.uri, insertConfigPos, ',\n ' + configBody);
480+
481+
// Format the document after the edit is applied
482+
fix.command = {
483+
command: 'editor.action.formatDocument',
484+
title: 'Format Document',
485+
};
486+
} catch {
487+
return [];
488+
}
489+
490+
fix.isPreferred = true;
491+
return [fix];
492+
},
493+
};
494+
495+
registerJsonCodeActionProvider(context, missingConfig);
496+
}

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export const DiagnosticCodes = {
7777
pluginConfigMissing: 'pluginConfigMissing',
7878
pluginConfigRequired: 'pluginConfigRequired',
7979
pluginConfigNotRequired: 'pluginConfigNotRequired',
80+
pluginConfigOptional: 'pluginConfigOptional',
8081
} as const;
8182

8283
/**

src/diagnostics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ const checkPluginConfiguration = (
336336
`${pluginName} can be configured with a configSection. Use '${pluginSnippet.config?.name}' snippet to create one.`,
337337
vscode.DiagnosticSeverity.Information,
338338
);
339+
diagnostic.code = getDiagnosticCode(DiagnosticCodes.pluginConfigOptional);
339340
diagnostics.push(diagnostic);
340341
}
341342
}

0 commit comments

Comments
 (0)