@@ -74,6 +74,7 @@ export const registerCodeActions = (context: vscode.ExtensionContext) => {
7474 registerOptionalConfigFixes ( context ) ;
7575 registerMissingConfigFixes ( context ) ;
7676 registerUnknownConfigPropertyFixes ( context ) ;
77+ registerInvalidConfigSectionFixes ( context ) ;
7778} ;
7879
7980function registerInvalidSchemaFixes (
@@ -741,3 +742,197 @@ function calculatePropertyDeleteRange(
741742
742743 return propertyRange ;
743744}
745+
746+ /**
747+ * Registers code actions for invalid config sections.
748+ * Provides "Remove section" and "Link to plugin" quick fixes.
749+ */
750+ function registerInvalidConfigSectionFixes ( context : vscode . ExtensionContext ) : void {
751+ // Register the command for linking a config section to a plugin
752+ context . subscriptions . push (
753+ vscode . commands . registerCommand (
754+ 'dev-proxy-toolkit.linkConfigSectionToPlugin' ,
755+ async ( documentUri : vscode . Uri , configSectionName : string ) => {
756+ const document = await vscode . workspace . openTextDocument ( documentUri ) ;
757+
758+ let documentNode : parse . ObjectNode ;
759+ try {
760+ documentNode = parse ( document . getText ( ) ) as parse . ObjectNode ;
761+ } catch {
762+ return ;
763+ }
764+
765+ const pluginsNode = getASTNode ( documentNode . children , 'Identifier' , 'plugins' ) ;
766+ if ( ! pluginsNode || pluginsNode . value . type !== 'Array' ) {
767+ return ;
768+ }
769+
770+ const pluginNodes = ( pluginsNode . value as parse . ArrayNode )
771+ . children as parse . ObjectNode [ ] ;
772+
773+ // Find plugins that don't have a configSection property
774+ const availablePlugins : { name : string ; node : parse . ObjectNode } [ ] = [ ] ;
775+ pluginNodes . forEach ( pluginNode => {
776+ const nameNode = getASTNode ( pluginNode . children , 'Identifier' , 'name' ) ;
777+ const configSectionNode = getASTNode ( pluginNode . children , 'Identifier' , 'configSection' ) ;
778+ if ( nameNode && ! configSectionNode ) {
779+ availablePlugins . push ( {
780+ name : ( nameNode . value as parse . LiteralNode ) . value as string ,
781+ node : pluginNode ,
782+ } ) ;
783+ }
784+ } ) ;
785+
786+ if ( availablePlugins . length === 0 ) {
787+ vscode . window . showInformationMessage ( 'All plugins already have a configSection.' ) ;
788+ return ;
789+ }
790+
791+ const selected = await vscode . window . showQuickPick (
792+ availablePlugins . map ( p => p . name ) ,
793+ { placeHolder : 'Select a plugin to link this config section to' }
794+ ) ;
795+
796+ if ( ! selected ) {
797+ return ;
798+ }
799+
800+ const selectedPlugin = availablePlugins . find ( p => p . name === selected ) ;
801+ if ( ! selectedPlugin ) {
802+ return ;
803+ }
804+
805+ const edit = new vscode . WorkspaceEdit ( ) ;
806+ const lastProperty = selectedPlugin . node . children [ selectedPlugin . node . children . length - 1 ] ;
807+ const insertPos = new vscode . Position (
808+ lastProperty . loc ! . end . line - 1 ,
809+ lastProperty . loc ! . end . column
810+ ) ;
811+
812+ edit . insert (
813+ documentUri ,
814+ insertPos ,
815+ `,\n "configSection": "${ configSectionName } "`
816+ ) ;
817+
818+ await vscode . workspace . applyEdit ( edit ) ;
819+ await vscode . commands . executeCommand ( 'editor.action.formatDocument' ) ;
820+ }
821+ )
822+ ) ;
823+
824+ const invalidConfigSection : vscode . CodeActionProvider = {
825+ provideCodeActions : ( document , range , context ) => {
826+ const currentDiagnostic = findDiagnosticByCode (
827+ context . diagnostics ,
828+ 'invalidConfigSection' ,
829+ range
830+ ) ;
831+
832+ if ( ! currentDiagnostic ) {
833+ return [ ] ;
834+ }
835+
836+ // Extract config section name from diagnostic message
837+ const match = currentDiagnostic . message . match ( / ^ C o n f i g s e c t i o n ' ( \w + ) ' / ) ;
838+ if ( ! match ) {
839+ return [ ] ;
840+ }
841+
842+ const configSectionName = match [ 1 ] ;
843+ const fixes : vscode . CodeAction [ ] = [ ] ;
844+
845+ // 1. "Remove section" fix
846+ try {
847+ const documentNode = parse ( document . getText ( ) ) as parse . ObjectNode ;
848+ const configSectionProperty = getASTNode (
849+ documentNode . children ,
850+ 'Identifier' ,
851+ configSectionName
852+ ) ;
853+
854+ if ( configSectionProperty ) {
855+ const removeFix = new vscode . CodeAction (
856+ `Remove '${ configSectionName } ' section` ,
857+ vscode . CodeActionKind . QuickFix
858+ ) ;
859+
860+ removeFix . edit = new vscode . WorkspaceEdit ( ) ;
861+
862+ const deleteRange = calculateConfigSectionDeleteRange (
863+ document ,
864+ configSectionProperty
865+ ) ;
866+ removeFix . edit . delete ( document . uri , deleteRange ) ;
867+
868+ removeFix . command = {
869+ command : 'editor.action.formatDocument' ,
870+ title : 'Format Document' ,
871+ } ;
872+
873+ removeFix . isPreferred = true ;
874+ fixes . push ( removeFix ) ;
875+ }
876+ } catch {
877+ // If AST parsing fails, skip the remove fix
878+ }
879+
880+ // 2. "Link to plugin" fix
881+ const linkFix = new vscode . CodeAction (
882+ `Link '${ configSectionName } ' to a plugin...` ,
883+ vscode . CodeActionKind . QuickFix
884+ ) ;
885+ linkFix . command = {
886+ command : 'dev-proxy-toolkit.linkConfigSectionToPlugin' ,
887+ title : 'Link config section to plugin' ,
888+ arguments : [ document . uri , configSectionName ] ,
889+ } ;
890+ fixes . push ( linkFix ) ;
891+
892+ return fixes ;
893+ } ,
894+ } ;
895+
896+ registerJsonCodeActionProvider ( context , invalidConfigSection ) ;
897+ }
898+
899+ /**
900+ * Calculate the range to delete for a config section property, including comma handling.
901+ */
902+ function calculateConfigSectionDeleteRange (
903+ document : vscode . TextDocument ,
904+ propertyNode : parse . PropertyNode ,
905+ ) : vscode . Range {
906+ const propRange = getRangeFromASTNode ( propertyNode ) ;
907+
908+ // Check if there's a comma after the property on the end line
909+ const endLineText = document . lineAt ( propRange . end . line ) . text ;
910+ const afterProp = endLineText . substring ( propRange . end . character ) ;
911+ const commaAfterMatch = afterProp . match ( / ^ \s * , / ) ;
912+
913+ if ( commaAfterMatch ) {
914+ // Delete from start of line to end of line (including comma)
915+ return new vscode . Range (
916+ new vscode . Position ( propRange . start . line , 0 ) ,
917+ new vscode . Position ( propRange . end . line + 1 , 0 )
918+ ) ;
919+ }
920+
921+ // No comma after - remove preceding comma if exists
922+ if ( propRange . start . line > 0 ) {
923+ const prevLineText = document . lineAt ( propRange . start . line - 1 ) . text ;
924+ if ( prevLineText . trimEnd ( ) . endsWith ( ',' ) ) {
925+ const commaPos = prevLineText . lastIndexOf ( ',' ) ;
926+ return new vscode . Range (
927+ new vscode . Position ( propRange . start . line - 1 , commaPos ) ,
928+ new vscode . Position ( propRange . end . line + 1 , 0 )
929+ ) ;
930+ }
931+ }
932+
933+ // Fallback: delete just the property lines
934+ return new vscode . Range (
935+ new vscode . Position ( propRange . start . line , 0 ) ,
936+ new vscode . Position ( propRange . end . line + 1 , 0 )
937+ ) ;
938+ }
0 commit comments