Refactoring with VS Code

Multiple cursors

Efficient Refactoring

Whilst watching an episode of Francesc Campoy’s just for func series I noticed that there was a major feature of VS Code that was going unused. Multiple cursors. This is one of the biggest productivitiy features of modern text editors.

In this example we are refactoring go code, however this is a language agnostic editor feature.

The problem at hand is to refactor the following code snippet (to remove global variables). Francesc did this by hand and (thankfully) sped up the video. It turns out that by using multiple cursors we can do it even quicker than the full speed video.

From this …

func main() {
	// Get flags
	flag.StringVar(&urlBase, "url", "http://example.com/v/%d", `The URL you wish to scrape, containing "%d" where the id should be substituted`)
	flag.IntVar(&idLow, "from", 0, "The first ID that should be searched in the URL - inclusive.")
	flag.IntVar(&idHigh, "to", 1, "The last ID that should be searched in the URL - exclusive")
	flag.IntVar(&concurrency, "concurrency", 1, "How many scrapers to run in parallel. (More scrapers are faster, but more prone to rate limiting or bandwith issues)")
	flag.StringVar(&outfile, "output", "output.csv", "Filename to export the CSV results")
	flag.StringVar(&nameQuery, "nameQuery", ".name", "JQuery-style query for the name element")
	flag.StringVar(&addressQuery, "addressQuery", ".address", "JQuery-style query for the address element")
	flag.StringVar(&phoneQuery, "phoneQuery", ".phone", "JQuery-style query for the phone element")
	flag.StringVar(&emailQuery, "emailQuery", ".email", "JQuery-style query for the email element")

	flag.Parse()
  
  // ... snip ...
}

Switch from flag.xxxxVar(&myGlobal... to myLocal : = flag.xxxx(...

To this …

func main() {
	// Get flags
	_urlBase := flag.String("url", "http://example.com/v/%d", `The URL you wish to scrape, containing "%d" where the id should be substituted`)
	idLow := flag.Int("from", 0, "The first ID that should be searched in the URL - inclusive.")
	idHigh := flag.Int("to", 1, "The last ID that should be searched in the URL - exclusive")
	concurrency := flag.Int("concurrency", 1, "How many scrapers to run in parallel. (More scrapers are faster, but more prone to rate limiting or bandwith issues)")
	outfile := flag.String("output", "output.csv", "Filename to export the CSV results")
	nameQuery := flag.String("nameQuery", ".name", "JQuery-style query for the name element")
	addressQuery := flag.String("addressQuery", ".address", "JQuery-style query for the address element")
	phoneQuery := flag.String("phoneQuery", ".phone", "JQuery-style query for the phone element")
	emailQuery := flag.String("emailQuery", ".email", "JQuery-style query for the email element")

	flag.Parse()

  // ... snip ...
}

TL;DR; commands

  1. Get a cursor on each interesting line
  2. Delete the common characters we don’t want Var(&
  3. Cut the variables out for each line variableName
  4. Paste the variables back to the correct place variableName
  5. Add new characters so the code complies :=

Detailed commands

Choosing your line anchors is much like using regular expressions, just without having to remember the syntax. Look for the interesting pattern on all the interesting lines. In our case Var should do it, except that a lower case var exists elsewhere in the code. So if we increase the size of the anchor (bigger anchors are more likeley to select what we want). Var( should get us the interesting lines, we will add the ampersand because we already know we want to delete it.

When working with multiple cursirs you can now perform any cursor relative movement operation; home, end, delete, backspace, select-to-end-of-word, move-to-end-of-word, etc. Note that if you have multiple cursors on a single line they will become a single cursor if they collide, e.g. press home or end.

  • Select one of the instances of Var(&
  • Select all instances ctrl+shift+l
  • delete the selected text, then add in an opening parenthisis “(”.
  • Word select the next whole-word, shift+ctrl+right-arrow (we can’t just right arrow because they words are not all the same length)
  • Cut the selected text ctrl+x
  • Move to the begginging of the line with home
  • Paste the variables in ctrl+v (this is the magic, each cursor has it’s own clipboard)
  • Add the remaining text, which is the same on each line, “:=
  • escape will end the multi-cursor session.

Alternative selection

One by one

Sometimes select all occurences is too wide so adding one instance at a time is better, use ctrl+d to select the next instance, to unselect the current and add the next press ctrl+d, ctrl+k.

Cursor for every line

If, as in our example, all the text is in a block we could have used a basic multi-line selection then add a cursor on every line with shift+alt+i. I’ve found that this command works on the selection plus wherever the cursor is so you can get an extra cursor if you are not careful.

Regular Expressions

You can use a regext to get multiple cursors; 1. ctrl+f –> Open find widegt. 2. alt+r –> Turn on regex mode. 3. Input search text –> Regex text or normal text. 4. alt+enter –> Select all matches.

Additional tools

I have also found the change case extension (wmaurer.change-case) for VS Code to be really useful. For example, switch from PascalCase to cammelCase when adding json attributes to Go or C# types.

The align extension (steve8708.Align) is really useful too for text layout with multiple cursors.

Key-binding

You can change the keys to drive this tool, these are the interesting operations.

[
  {"key": "ctrl+shift+l",
  "command": "editor.action.selectHighlights",
  "when": "editorFocus"
  },
  {"key": "ctrl+d",
  "command": "editor.action.addSelectionToNextFindMatch",
  "when": "editorFocus" 
  },
  {"key": "ctrl+k ctrl+d",
  "command": "editor.action.moveSelectionToNextFindMatch",
  "when": "editorFocus" 
  },
  {"key": "escape",
  "command": "removeSecondaryCursors",
  "when": "editorHasMultipleSelections && editorTextFocus" 
  },
  {"key": "shift+alt+i",
  "command": "editor.action.insertCursorAtEndOfEachLineSelected",
  "when": "editorTextFocus" 
  },
]

Links