visual-studio-codevscode-snippets

How to create a VS Code snippet which toggles a line prefix


I'm trying to write a VS Code snippet which can toggle the character sequence // ? at the start of the current line. When I run it, i want a line such as this:

var x = 10

to turn into:

// ?var x = 10

and visa versa.

This is similar to the built in comment toggling functionality (Cmd + /), but with a custom prefix.


Solution

  • There are at least 2 approaches to consider.

    1. Modify which comment characters are used for the languages you are interested in. Using an extension I wrote, Custom Language Properties, this is very easy to do. A quick demo:

    demo toggling custom comments with extension

    The advantages of this are it is very easy to set up and as you can see you can toggle multiple lines easily because the built-in commands Toggle Line Comment (editor.action.commentLine) or Add Line Comment (editor.action.addCommentLine) use the setting you created. This means whenever you use such commands they will do what you want. So you could use a line comment command in a macro and it would just work. Demo that shows copying the current line down and commenting the original line:

    macro comment and duplicate line down

    The above demo uses this keybinding:

    {
      "key": "ctrl+shift+/",
      "command": "runCommands",
      "args": {
        "commands": [
          "editor.action.copyLinesDownAction",
          "cursorUp",
          "editor.action.addCommentLine",  // uses your comment characters from the setting above
          "cursorDown"        
        ]
      },
      "when": "editorTextFocus"
    }
    

    1. Just make a snippet/keybinding that will toggle those prefix characters on and off. This can be done with a single keybinding that uses the regex to detect whether those prefix characters are there (if so, remove them) or are not there (if so, add them):
     {
        "key": "ctrl+/",       // whatever keybinding you want, acts as a toggle
        "command": "runCommands",
        "args": { 
         "commands": 
           [
             "cursorEnd",             // so cursor stays on same line
             "cursorHomeSelect",
             {
               "command": "editor.action.insertSnippet",
               "args": { 
    
                 "snippet": "${TM_SELECTED_TEXT/(\\/\\/ \\?)(.+)|(.+)/$2${3:+\/\/ ?}$3/}"
               }
             }
           ]
       },
       // "when": "editorLangId == javascript || editorLangId == typescript"
       "when": "resourceExtname =~ /\\.(js|ts|r)(?!on)/"
     }
    

    The regex (\\/\\/ \\?)(.+)|(.+) puts the prefix characters into capture group 1 (if they exist) and the rest of the line into either capture group 2 or 3 (only one of which will be defined).

    Capture group 1 is actually never used so the prefix characters don't need to be in a capture group but the separation in the regex helps to understand what is going on. The regex will only populate capture group 2 if there are those prefix characters. And since we never add $1 to the transform group 1 is effectively removed if it ever existed.

    The transform part of the snippet: $2${3:+\/\/ ?}$3 will insert group 2 (which may be empty), then IF there is a group 3 (there will only be a group 3 if there were no prefix characters on the line) and then add group 3 (the rest of the line). The regex has an alternation or | so only one of groups 2 and 3 will ever have content - there is no problem putting a reference to an empty capture group in the transform.

    A demo of this second approach would be the same as the first demo but the multiple line commenting wouldn't work (only the first line would be commented). And the various line comment commands like Toggle Line Comment and Add Line Comment wouldn't use those prefix characters. But maybe that is what you want and so method (2) would be better for you.