Skip to content

πŸ’Ύ πŸ“¦ βœ… Automate everything in VS code

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
LICENSE.md
Notifications You must be signed in to change notification settings

jeff-hykin/macro-commander

Repository files navigation

What does this do?

It lets you write a quick sequence of commands/scripts to automate VS Code tasks.

Example Usage (Code Examples are Futher Below)

  • A run command that opens up a terminal, starts an SSH connection, and then runs commands on the SSH server
  • A new-project command that goes to where ever you typically save projects, uses the GUI to ask for the name of the project, creates the folder, creates a .gitignore with your preferences, runs an init command, and then opens the folder in your current VS Code window.
  • A command that opens a project folder, pulls the latest changes, opens several files in that folder, and then displays the recent changes in those files.
  • A folder-specific start command, that pulls the latest changes, installs dependences, formats files, and then opens the debugger with a specific file.

How do I use it?

  1. Find the name of commands you want to run. You can do this by going to the VS Code keybindings.json (go to gear-icon -> keybindings, then press the β†ͺπŸ“„ in the top right corner) All of the "command":'s (ex: "command": "workbench.action.terminal.new") can be copied and pasted into the macro
  2. Open up your VS Code settings.json and create a new section like this: (go to gear-icon -> settings, then press the {}'s in the top right corner)
"macros": {
    "exampleMacro1": [
        // a simple command to open a new terminal
        "workbench.action.terminal.new",
        
        // OR a complex command (e.g. has arguments)
        { "command": "COMMAND_HERE", "args":/*stuff*/ }
        
        // OR javascript
        { "javascript": [ "window.showInformationMessage('hello')" ] }
    ]
}
  1. To run the macro open the command pallet (cmd+shift+P or ctrl+shift+P) and type run macro then pick which one you want to run.
  2. To create a keybinding to the macro, simply open the Keyboard Shortcuts (Gear-icon -> Keyboard Shortcuts) and start to type "macros". All of the macros have a "macros." prefix.

Command Options

Every element in the macro array should be one of these:

  • string = simple command
  • command with args is an object like this {"command": "COMMAND_HERE", "args":/*stuff*/}
  • command + javascript using javascript-injections like this: { "injections": [], "command": "COMMAND_HERE", "args":/*stuff*/}
  • run javascript likes this: { "javascript": [] }
  • call a hidden console (childProcess) like this { "injections": [], "hiddenConsole": [] }

NOTE:

  1. The javascript runs inside of an async function, meaning you can use the await keyword
  2. Javascript has access to:
  • the vscode object: (vscode.commands, vscode.env, vscode.workspace, vscode.tasks, etc.) is documented here: https://code.visualstudio.com/api/
  • the window object, a shortcut to vscode.window.
  • the path object (from node path)
  • the fs object (from node fs)
  • the macroTools object (helpers from this library)
  1. The "withResultOf" argument can be a single value (3.14159 || null) or multiple statements (await window.showInputBox(); console.log("hi")).

Quality of Life Tools

If you're writing JS macros:

  • Use the "Macros: JS to JSON" command to make it easier to write macros (select JS code, then call the command)
  • Use the "Macros: JSON to JS" command to make it easier to read/edit macros

What are some examples?

See also Level up your Coding with Macros

"macros": {
    "terminalExample1": [
        // a simple command to open a new terminal
        "workbench.action.terminal.new",
        // a command with arguments, that sends text to the terminal
        {
            "command": "workbench.action.terminal.sendSequence", 
            // the "text" arg was decided by VS Code (not me)
            "args": { "text": "echo hello\n" }
        },
    ],
    "terminalExample2" : [
        // Run some javascript
        {
            "javascript": [
                // docs for showInputBox: https://vscode-api.js.org/interfaces/vscode.InputBoxOptions.html
                "sharedMacroInfo.personName = await window.showInputBox({ title: `Whats your name?` })",
            ]
        },
        // a simple command to open a new terminal
        "workbench.action.terminal.new",
        // combine javascript and command
        {
            "injections" : [
                { "replace": "$personName", "withResultOf": "macroTools.escapeShellArg(sharedMacroInfo.personName)" },
                { "replace": "$selectedText", "withResultOf": "macroTools.escapeShellArg(window.activeTextEditor.document.getText(window.activeTextEditor.selections[0]))" },
                { "replace": "$currentFile", "withResultOf": "macroTools.escapeShellArg(window.activeTextEditor.document.uri.fsPath)" },
                { "replace": "$currentFolder", "withResultOf": "macroTools.escapeShellArg(vscode.workspace.rootPath)" },
            ],
            "command": "workbench.action.terminal.sendSequence",
            "args": { "text": "echo hi $personName\necho 'you selected' $selectedText\necho 'the current file is: '$currentFile\necho the current folder is: $currentFolder\n" }
        },
    ],
    "userInputExample1" : [
        // javascript execution (see https://code.visualstudio.com/api/extension-capabilities/common-capabilities)
        {
            // this has access to the `vscode` object, the `window` object and the `path` object (from node path)
            // the javascript is also run inside of an async function, meaning you can use the `await` keyword
            "javascript": "window.showInformationMessage(`You entered: ${await window.showInputBox()}`)"
        },
    ],
    "userInputExample2" : [
        {
            "javascript": [
                "let response = await window.showInputBox()",
                "let selectedText = window.activeTextEditor.document.getText(window.activeTextEditor.selections[0])",
                "await window.showInformationMessage(`You entered: ${response}`)",
            ]
        },
    ],
    "fileInputExample1" : [
        {
            "javascript": [
                "let cursorLineNumbers = window.activeTextEditor.selections.map(each=>each.start.line)",
                "let cursorCharNumbers = window.activeTextEditor.selections.map(each=>each.start.character)",
                "let textContent = window.activeTextEditor.document.lineAt(cursorLineNumbers[0]).text",
            ]
        },
    ],
    "userInputExample3" : [
        {
            "javascriptPath": "~/vs_code_stuff/macros/example3.js",
            // if you open up that file^  it could contain:
            //     const { window } = require("vscode")
            //     const vscode = require("vscode")
            //     const path = require("path")
            //     const fs = require("fs")
            // 
            //     await window.showInformationMessage(`Howdy!`)
            
            // NOTE: you cant use "import" or "export" because this script is evaled! not imported
        },
    ],
    "userInputExample4" : [
        {
            "javascriptPath": "./macros/build.js",
            // this path will be relative to the current WORKSPACE
            // if there is no active workspace, it will tell you to activate one (e.g. error)
        },
    ],
    "sharedInfo1" : [
        {
            // use this varible to share data between macros, and within the same macro
            "javascript": "sharedMacroInfo.prevMessage = 'howdy'",
        },
        {
            "javascript": "window.showInformationMessage(sharedMacroInfo.prevMessage)",
        },
    ],
    "javascriptPlusTerminalExample" : [
        // run a hidden console command (runs in the background)
        {
            // NOTE: don't start a command in a hiddenConsole
            //       that doesn't finish! there's no good way 
            //       of killing/canceling it
            // 
            // this echo will never be seen
            "hiddenConsole": [
                "touch .gitignore",
                "echo hello"
            ]
        },
        // combine javascript and hidden console commands
        {
            "injections" : [
                { "replace": "$currentFolder", "withResultOf": "vscode.workspace.rootPath" },
            ],
            "hiddenConsole": [
               "cd \"$currentFolder\"", 
               "touch .gitignore"
            ]
        },
    ],
    "terminalWithBashFunctions" : [
        {
            "injections" : [
                { "replace": "$currentFolder", "withResultOf": "vscode.workspace.rootPath" }
            ],
            // I wanted to use aliases defined in my bash profile
            // here's a hacky way of doing that
            "hiddenConsole": [
                "bash <<HEREDOC",
                "    source ~/.bash_profile",
                "    cd \"$currentFolder\"",
                "    echo now I can use aliases",
                "HEREDOC",
            ]
        }
    ],
    "exampleOfCommonInjections" : [
        {
            "injections" : [
                { "replace": "$userInput",       "withResultOf": "await window.showInputBox()" },
                { "replace": "$currentFolder",   "withResultOf": "vscode.workspace.rootPath" },
                { "replace": "$currentFile",     "withResultOf": "window.activeTextEditor.document.uri.fsPath" },
                { "replace": "$currentFileName", "withResultOf": "path.basename(window.activeTextEditor.document.uri.fsPath)" },
                { "replace": "$currentFileNameNoExtension", "withResultOf": "path.basename(window.activeTextEditor.document.uri.fsPath).replace(/\\.[^/.]+$/, '')" },
                { "replace": "$currentFileDir", "withResultOf": "path.dirname(window.activeTextEditor.document.uri.fsPath)" },
            ],
            "hiddenConsole" : "echo $userInput"
        }
    ],
    "WriteToOutputExample": [
        {
            "javascript": [
                //create a new OUTPUT log, with name "MyLog"
                "const myOutput = window.createOutputChannel('MyLog'); ",   
    
                //write a message to OUTPUT log                              
                "await myOutput.appendLine('Lorem ipsum dolor sit amet, consectetur adipisci elit.'); ",  

                //show the OUTPUT log       
                "await myOutput.show(); ",                                                                   
            ],
        },
    ],
    "unMultiSelectLast": [
        // For when you Ctrl-Click to multiselect 10 times and on the eleventh get it wrong. 
        // Just press Ctrl-0 (or whatever key you assign) to unselect the eleventh, then carry on.
        // (See also, https://github.com/danseethaler/vscode-tab-through-selections, for more along this line.)
        {
            "javascript": [
                "const editor = window.activeTextEditor;",
                "const newSelections = editor.selections.slice(0, editor.selections.length - 1);",
                "editor.selections = newSelections;"
            ]
        }
    ],
    "transformToSnake": [
        // A multi-select friendly macro to convert from CamelCase to snake_case.
        // If any particular selection is empty (just a cursor), this will automatically expand it to the whole word first.
        // (Kudos to https://stackoverflow.com/users/398630/brainslugs83 for some pointers)
        {
            "javascript": [
                "let editor = window.activeTextEditor;",
                "expandWords();",
                "doTransform(0);",
                "function expandWords() { let sels = editor.selections; let i = sels.length-1;",
                "  while (i >=0) { let sel = sels[i];",
                "    if (sel.isEmpty) {r = editor.document.getWordRangeAtPosition(sel.start); sels[i] = new vscode.Selection(r.start, r.end);}",
                "    i--; }",
                "  editor.selections = sels;",
                "}",
                "function doTransform(i) { let sels = editor.selections;",
                "  if (i < 0 || i >= sels.length) { return; }",
                "  let sel = sels[i];",
                "  let word_matches = editor.document.getText(sel).matchAll(/([a-z]+|[A-Z][a-z]*|[^A-Za-z]+)/g);",
                "  let words = [];",
                "  for (const match of word_matches) {words.push(match[0].toLowerCase())};",
                "  editor.edit(eb => {eb.replace(sel, words.join('_'))}).then(x => { doTransform(i+1); });",
                "}"
            ]
        }
    ],
    "transformToCamel": [
        // Same as transformToSnake, but vice versa
        {
            "javascript": [
                "let editor = window.activeTextEditor;",
                "expandWords();",
                "doTransform(0);",
                "function expandWords() { let sels = editor.selections; let i = sels.length-1;",
                "  while (i >=0) { let sel = sels[i];",
                "    if (sel.isEmpty) {r = editor.document.getWordRangeAtPosition(sel.start); sels[i] = new vscode.Selection(r.start, r.end);}",
                "    i--; }",
                "  editor.selections = sels;",
                "}",
                "function doTransform(i) { let sels = editor.selections;",
                "  if (i < 0 || i >= sels.length) { return; }",
                "  let sel = sels[i];",
                "  let words = editor.document.getText(sel).split('_')",
                "  let camel_words = words.map(function(w) {return w[0].toUpperCase() + w.slice(1,).toLowerCase()});",
                "  editor.edit(eb => {eb.replace(sel, camel_words.join(''))}).then(x => { doTransform(i+1); });",
                "}"
            ]
        }
    ],
    "replaceAllExample1": [
        {
            "javascript": [
                //save the current clipboard
                "var oldClip = await vscode.env.clipboard.readText(); ",    

                //copies the selected text to the clipboard
                "await vscode.commands.executeCommand('editor.action.clipboardCopyAction'); ",
                "var testoSelezionato = await vscode.env.clipboard.readText(); ",   

                //replace all "gatta" to "##########"
                "var nuovoTesto = testoSelezionato.replace(/gatta/g, '##########'); ",   

                //paste the new text
                "if( nuovoTesto != testoSelezionato ) { ",          
                "   await vscode.env.clipboard.writeText(nuovoTesto); ", 
                "   await vscode.commands.executeCommand('editor.action.clipboardPasteAction'); ",
                "} ",          

                //restore the original clipboard
                "await vscode.env.clipboard.writeText(oldClip); ", 
            ],
        },
    ],
    "replaceAllExample2": [
        { 
            "javascript": [
                // parameters
                "var cerca       = 'gatta'; ",
                "var opzioni     = 'g'; ",             // g = global; i = ignorecase     
                "var sostituisci = '##########'; ",
                // get the currently active editor
                "const myEditor = vscode.window.activeTextEditor; ",
                "if (myEditor) {",
                    // perform an edit on the document associated with this text editor
                    "myEditor.edit(myEditBuilder => { ",

                        // loop for each multi-cursor
                        "myEditor.selections.forEach(rangeSelezione => {",
                            // get selected text from a single multi-cursor
                            "var testoSelezionato = myEditor.document.getText(rangeSelezione);",
                            // replace all 
                            "var nuovoTesto = testoSelezionato.replace(new RegExp(cerca, opzioni), sostituisci);",
                            // if the text has changed, I write it in the document
                            "if(nuovoTesto != testoSelezionato) { ",
                                "myEditBuilder.replace(rangeSelezione, nuovoTesto);",
                            "}",
                        "});",
                    "});",
                "}",
            ]
        }
    ],    
}

Some Common Macro Steps

Desired Action Macro Step (example)
execute a snippet {"command": "type", "args": {"text": "mySnippetPrefixHere"}}, "insertSnippet" ]
open a new terminal "workbench.action.terminal.new"
type into the terminal { "command": "workbench.action.terminal.sendSequence", "args": { "text": "echo hello\n" } }
message box { "javascript": "window.showInformationMessage(You entered: ${await window.showInputBox()})" }
user input { "javascript": [ "let response = await window.showInputBox()" ], ...
user output ... [ "await window.showInformationMessage(You entered: ${response})" ] }
Copy to clipboard "editor.action.clipboardCopyAction"

Some Common Injections:

Desired Value JavaScript Expression
$userInput await window.showInputBox()
$currentFolder vscode.workspace.rootPath
$currentFile window.activeTextEditor.document.uri.fsPath
$currentFileName path.basename(window.activeTextEditor.document.uri.fsPath)
$currentFileNameNoExtension path.basename(window.activeTextEditor.document.uri.fsPath).replace(/\.[^/.]+$/, '')
$currentFileDir path.dirname(window.activeTextEditor.document.uri.fsPath)
$selectionText window.activeTextEditor.document.getText(window.activeTextEditor.selection)
$clipboardText vscode.env.clipboard.readText()
$preferedLanguage vscode.env.language ("en-US")
$machineId vscode.env.machineId (The name of computer you are running on)
$sessionId vscode.env.sessionId (A unique string that changes when VS Code restarts)
$shellName vscode.env.shell (The name of the default terminal shell)

Some Useful JavaScript (assuming: const doc = window.activeTextEditor.document)

Category Desired Action JavaScript Code
Document text The entire text doc.getText()
Document text Part of the text doc.getText(range)
Document text Range of the word at cursor doc.getWordRangeAtPosition(position)
Document text The line of text at cursor doc.lineAt(line: number)
Document text The line of text at a position doc.lineAt(position) // a Position is a line number/char number pair
Document text Convert position to overall offset doc.offsetAt(position)
Document text Convert overall offset to position doc.positionAt(offset: number)
Document info EOL style doc.eol // enum (LF=1, CRLF=2)
Document info File name (doc.isUntitled ? "" : doc.fileName)
Document info Needs saving doc.isDirty
Document info Line count doc.lineCount
Document info Save to disk doc.save()
Multi-Select If multi-selected... if (window.activeTextEditor.selections.length > 1) { ... };
Multi-Select Only act if index is in range if (window.activeTextEditor.selections[index]) { ... };
Multi-Select Scroll to a particular selection window.activeTextEditor.revealRange(window.activeTextEditor.selections[index]);
Multi-Select Select only the last selection window.activeTextEditor.selection = window.activeTextEditor.selections.pop();

Who made this?


polyglot-jones improved the documentation and added three big examples (unMultiSelectLast, transformToSnake, transformToCamel)
The old extension was made by geddski
I modified it to have

  • in-order execution
  • javascript / javascript-injections
  • async await
  • keybindings + named execution
  • error message pop-ups
  • auto reload when settings was edited
  • and a few other things

About

πŸ’Ύ πŸ“¦ βœ… Automate everything in VS code

Topics

Resources

License

MIT, MIT licenses found

Licenses found

MIT
LICENSE
MIT
LICENSE.md

Stars

Watchers

Forks

Packages

No packages published