@@ -11,6 +11,7 @@ import { activeWorkspaceFolder, describeWorkspaceEnvironment } from "../workspac
1111
1212const execFileAsync = promisify ( execFile ) ;
1313const STRUCTURED_FILE_EXTENSIONS = new Set ( [ ".json" , ".yaml" , ".yml" , ".toml" ] ) ;
14+ const MARKDOWN_FILE_EXTENSIONS = new Set ( [ ".md" , ".markdown" , ".mdx" ] ) ;
1415
1516export type TidyFix = "ensure-final-newline" | "trim-trailing-whitespace" | "normalize-eol-lf" ;
1617
@@ -297,6 +298,255 @@ export async function runQuickAction(): Promise<void> {
297298 await vscode . env . clipboard . writeText ( value ) ;
298299 await vscode . window . showInformationMessage ( `${ selector } = ${ value } (copied to clipboard)` ) ;
299300 }
301+ } ,
302+ {
303+ label : "Delete structured value" ,
304+ description : "Remove a key from JSON, YAML, or TOML with diff preview" ,
305+ detail : "Builds `patchloom doc delete <file> <selector>`" ,
306+ run : async ( ) => {
307+ const target = await pickWorkspaceFileTarget ( "Select a JSON, YAML, or TOML file for Patchloom doc delete" ) ;
308+ if ( ! target ) {
309+ return ;
310+ }
311+
312+ if ( ! isStructuredDocumentPath ( target . absolutePath ) ) {
313+ await vscode . window . showWarningMessage (
314+ `${ target . relativePath } is not a supported JSON, YAML, or TOML file for Patchloom doc delete.`
315+ ) ;
316+ return ;
317+ }
318+
319+ const selector = await vscode . window . showInputBox ( {
320+ prompt : "Selector path to delete" ,
321+ placeHolder : "scripts.deprecated" ,
322+ validateInput : ( value ) => value . length > 0 ? undefined : "Selector is required."
323+ } ) ;
324+ if ( selector === undefined ) {
325+ return ;
326+ }
327+
328+ await previewAndMaybeApply ( binaryPath , target , buildDocDeleteQuickAction ( target . absolutePath , selector ) ) ;
329+ }
330+ } ,
331+ {
332+ label : "Merge into structured file" ,
333+ description : "Merge a partial JSON object into a config file" ,
334+ detail : "Builds `patchloom doc merge <file> --value <json>`" ,
335+ run : async ( ) => {
336+ const target = await pickWorkspaceFileTarget ( "Select a JSON, YAML, or TOML file for Patchloom doc merge" ) ;
337+ if ( ! target ) {
338+ return ;
339+ }
340+
341+ if ( ! isStructuredDocumentPath ( target . absolutePath ) ) {
342+ await vscode . window . showWarningMessage (
343+ `${ target . relativePath } is not a supported JSON, YAML, or TOML file for Patchloom doc merge.`
344+ ) ;
345+ return ;
346+ }
347+
348+ const value = await vscode . window . showInputBox ( {
349+ prompt : "Partial JSON object to merge" ,
350+ placeHolder : '{"debug": true, "logLevel": "verbose"}' ,
351+ validateInput : ( input ) => input . length > 0 ? undefined : "Value is required."
352+ } ) ;
353+ if ( value === undefined ) {
354+ return ;
355+ }
356+
357+ await previewAndMaybeApply ( binaryPath , target , buildDocMergeQuickAction ( target . absolutePath , value ) ) ;
358+ }
359+ } ,
360+ {
361+ label : "Append to array" ,
362+ description : "Append a value to a JSON, YAML, or TOML array" ,
363+ detail : "Builds `patchloom doc append <file> <selector> <value>`" ,
364+ run : async ( ) => {
365+ const target = await pickWorkspaceFileTarget ( "Select a JSON, YAML, or TOML file for Patchloom doc append" ) ;
366+ if ( ! target ) {
367+ return ;
368+ }
369+
370+ if ( ! isStructuredDocumentPath ( target . absolutePath ) ) {
371+ await vscode . window . showWarningMessage (
372+ `${ target . relativePath } is not a supported JSON, YAML, or TOML file for Patchloom doc append.`
373+ ) ;
374+ return ;
375+ }
376+
377+ const selector = await vscode . window . showInputBox ( {
378+ prompt : "Selector path to the array" ,
379+ placeHolder : "dependencies" ,
380+ validateInput : ( value ) => value . length > 0 ? undefined : "Selector is required."
381+ } ) ;
382+ if ( selector === undefined ) {
383+ return ;
384+ }
385+
386+ const value = await vscode . window . showInputBox ( {
387+ prompt : "Value to append" ,
388+ placeHolder : '"new-item"' ,
389+ validateInput : ( input ) => input . length > 0 ? undefined : "Value is required."
390+ } ) ;
391+ if ( value === undefined ) {
392+ return ;
393+ }
394+
395+ await previewAndMaybeApply ( binaryPath , target , buildDocAppendQuickAction ( target . absolutePath , selector , value ) ) ;
396+ }
397+ } ,
398+ {
399+ label : "Append table row" ,
400+ description : "Append a row to a markdown table under a heading" ,
401+ detail : "Builds `patchloom md table-append <file> --heading <h> --row <row>`" ,
402+ run : async ( ) => {
403+ const target = await pickWorkspaceFileTarget ( "Select a markdown file for Patchloom table-append" ) ;
404+ if ( ! target ) {
405+ return ;
406+ }
407+
408+ if ( ! isMarkdownPath ( target . absolutePath ) ) {
409+ await vscode . window . showWarningMessage (
410+ `${ target . relativePath } is not a markdown file.`
411+ ) ;
412+ return ;
413+ }
414+
415+ const heading = await vscode . window . showInputBox ( {
416+ prompt : "Heading containing the table" ,
417+ placeHolder : "## API" ,
418+ validateInput : ( value ) => value . length > 0 ? undefined : "Heading is required."
419+ } ) ;
420+ if ( heading === undefined ) {
421+ return ;
422+ }
423+
424+ const row = await vscode . window . showInputBox ( {
425+ prompt : "Table row to append (pipe-delimited)" ,
426+ placeHolder : "| /users | List users | GET |" ,
427+ validateInput : ( value ) => value . length > 0 ? undefined : "Row is required."
428+ } ) ;
429+ if ( row === undefined ) {
430+ return ;
431+ }
432+
433+ await previewAndMaybeApply ( binaryPath , target , buildMdTableAppendQuickAction ( target . absolutePath , heading , row ) ) ;
434+ }
435+ } ,
436+ {
437+ label : "Upsert bullet" ,
438+ description : "Add a bullet under a markdown heading (idempotent)" ,
439+ detail : "Builds `patchloom md upsert-bullet <file> --heading <h> --bullet <text>`" ,
440+ run : async ( ) => {
441+ const target = await pickWorkspaceFileTarget ( "Select a markdown file for Patchloom upsert-bullet" ) ;
442+ if ( ! target ) {
443+ return ;
444+ }
445+
446+ if ( ! isMarkdownPath ( target . absolutePath ) ) {
447+ await vscode . window . showWarningMessage (
448+ `${ target . relativePath } is not a markdown file.`
449+ ) ;
450+ return ;
451+ }
452+
453+ const heading = await vscode . window . showInputBox ( {
454+ prompt : "Heading to add the bullet under" ,
455+ placeHolder : "## Rules" ,
456+ validateInput : ( value ) => value . length > 0 ? undefined : "Heading is required."
457+ } ) ;
458+ if ( heading === undefined ) {
459+ return ;
460+ }
461+
462+ const bullet = await vscode . window . showInputBox ( {
463+ prompt : "Bullet text (without leading dash)" ,
464+ placeHolder : "Run make check before committing" ,
465+ validateInput : ( value ) => value . length > 0 ? undefined : "Bullet text is required."
466+ } ) ;
467+ if ( bullet === undefined ) {
468+ return ;
469+ }
470+
471+ await previewAndMaybeApply ( binaryPath , target , buildMdUpsertBulletQuickAction ( target . absolutePath , heading , bullet ) ) ;
472+ }
473+ } ,
474+ {
475+ label : "Replace markdown section" ,
476+ description : "Replace content under a markdown heading" ,
477+ detail : "Builds `patchloom md replace-section <file> --heading <h> --content <text>`" ,
478+ run : async ( ) => {
479+ const target = await pickWorkspaceFileTarget ( "Select a markdown file for Patchloom replace-section" ) ;
480+ if ( ! target ) {
481+ return ;
482+ }
483+
484+ if ( ! isMarkdownPath ( target . absolutePath ) ) {
485+ await vscode . window . showWarningMessage (
486+ `${ target . relativePath } is not a markdown file.`
487+ ) ;
488+ return ;
489+ }
490+
491+ const heading = await vscode . window . showInputBox ( {
492+ prompt : "Heading of the section to replace" ,
493+ placeHolder : "## Unreleased" ,
494+ validateInput : ( value ) => value . length > 0 ? undefined : "Heading is required."
495+ } ) ;
496+ if ( heading === undefined ) {
497+ return ;
498+ }
499+
500+ const content = await vscode . window . showInputBox ( {
501+ prompt : "New section content" ,
502+ placeHolder : "- New feature added" ,
503+ validateInput : ( value ) => value . length > 0 ? undefined : "Content is required."
504+ } ) ;
505+ if ( content === undefined ) {
506+ return ;
507+ }
508+
509+ await previewAndMaybeApply ( binaryPath , target , buildMdReplaceSectionQuickAction ( target . absolutePath , heading , content ) ) ;
510+ }
511+ } ,
512+ {
513+ label : "Undo last change" ,
514+ description : "Restore files from the last patchloom backup" ,
515+ detail : "Runs `patchloom undo`" ,
516+ run : async ( ) => {
517+ const folder = await activeWorkspaceFolder ( {
518+ promptIfMany : true ,
519+ placeHolder : "Select workspace folder for Patchloom undo"
520+ } ) ;
521+ if ( ! folder ) {
522+ await vscode . window . showWarningMessage ( "Open a workspace folder before running Patchloom undo." ) ;
523+ return ;
524+ }
525+
526+ const confirm = await vscode . window . showWarningMessage (
527+ "Undo the last patchloom edit? This restores files from backup." ,
528+ { modal : true } ,
529+ "Undo"
530+ ) ;
531+ if ( confirm !== "Undo" ) {
532+ return ;
533+ }
534+
535+ const action = buildUndoQuickAction ( folder . uri . fsPath ) ;
536+ const result = await executePatchloom ( binaryPath , action . args , folder . uri . fsPath ) ;
537+
538+ if ( result . exitCode !== 0 ) {
539+ const message = result . stderr . includes ( "no backup" )
540+ ? "No patchloom backup to undo."
541+ : `Patchloom undo failed: ${ formatCliOutput ( result ) } ` ;
542+ await vscode . window . showWarningMessage ( message ) ;
543+ return ;
544+ }
545+
546+ const log = getPatchloomLog ( ) ;
547+ log ?. show ( ) ;
548+ await vscode . window . showInformationMessage ( "Patchloom undo complete. Restored files shown in the output channel." ) ;
549+ }
300550 }
301551 ] ;
302552
@@ -382,6 +632,73 @@ export function buildDocGetQuickAction(targetPath: string, selector: string): Pl
382632 } ;
383633}
384634
635+ export function buildDocDeleteQuickAction ( targetPath : string , selector : string ) : PlannedQuickAction {
636+ return {
637+ title : `Delete ${ selector } from ${ path . basename ( targetPath ) } ` ,
638+ targetPath,
639+ targetArgIndices : [ 2 ] ,
640+ args : [ "doc" , "delete" , targetPath , selector ]
641+ } ;
642+ }
643+
644+ export function buildDocMergeQuickAction ( targetPath : string , value : string ) : PlannedQuickAction {
645+ return {
646+ title : `Merge into ${ path . basename ( targetPath ) } ` ,
647+ targetPath,
648+ targetArgIndices : [ 2 ] ,
649+ args : [ "doc" , "merge" , targetPath , "--value" , value ]
650+ } ;
651+ }
652+
653+ export function buildDocAppendQuickAction ( targetPath : string , selector : string , value : string ) : PlannedQuickAction {
654+ return {
655+ title : `Append to ${ selector } in ${ path . basename ( targetPath ) } ` ,
656+ targetPath,
657+ targetArgIndices : [ 2 ] ,
658+ args : [ "doc" , "append" , targetPath , selector , value ]
659+ } ;
660+ }
661+
662+ export function buildMdTableAppendQuickAction ( targetPath : string , heading : string , row : string ) : PlannedQuickAction {
663+ return {
664+ title : `Append table row under "${ heading } " in ${ path . basename ( targetPath ) } ` ,
665+ targetPath,
666+ targetArgIndices : [ 2 ] ,
667+ args : [ "md" , "table-append" , targetPath , "--heading" , heading , "--row" , row ]
668+ } ;
669+ }
670+
671+ export function buildMdUpsertBulletQuickAction ( targetPath : string , heading : string , bullet : string ) : PlannedQuickAction {
672+ return {
673+ title : `Upsert bullet under "${ heading } " in ${ path . basename ( targetPath ) } ` ,
674+ targetPath,
675+ targetArgIndices : [ 2 ] ,
676+ args : [ "md" , "upsert-bullet" , targetPath , "--heading" , heading , "--bullet" , bullet ]
677+ } ;
678+ }
679+
680+ export function buildMdReplaceSectionQuickAction ( targetPath : string , heading : string , content : string ) : PlannedQuickAction {
681+ return {
682+ title : `Replace "${ heading } " in ${ path . basename ( targetPath ) } ` ,
683+ targetPath,
684+ targetArgIndices : [ 2 ] ,
685+ args : [ "md" , "replace-section" , targetPath , "--heading" , heading , "--content" , content ]
686+ } ;
687+ }
688+
689+ export function buildUndoQuickAction ( workspacePath : string ) : PlannedQuickAction {
690+ return {
691+ title : "Undo last patchloom change" ,
692+ targetPath : workspacePath ,
693+ targetArgIndices : [ ] ,
694+ args : [ "undo" , "--apply" ]
695+ } ;
696+ }
697+
698+ export function isMarkdownPath ( filePath : string ) : boolean {
699+ return MARKDOWN_FILE_EXTENSIONS . has ( path . extname ( filePath ) . toLowerCase ( ) ) ;
700+ }
701+
385702export function isStructuredDocumentPath ( filePath : string ) : boolean {
386703 return STRUCTURED_FILE_EXTENSIONS . has ( path . extname ( filePath ) . toLowerCase ( ) ) ;
387704}
0 commit comments