DIY VS Code Extension 1: Development
Tags: vscode, typescript, javascript
Published: 2020-10-12 03:20:48

Have you ever wished for a little extra feature in VS Code, and nothing turns up in a search?  Fear not.  It's time to DIY.

---

[![Tools by Barn Images on Unsplash](https://images.unsplash.com/photo-1426927308491-6380b6a9936f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2851&q=80)](https://unsplash.com/photos/t5YUoHW6zRo)

A few days ago I realized I had once again swapped my true/false cases in a ternary expression.  It's always a little annoying to reformat it, switch the punctuation, redo whitespace, etc.  So I made [`SwapTernary`](https://marketplace.visualstudio.com/items?itemName=256hz.swap-ternary)!

If you want to try it out, it's [here](https://marketplace.visualstudio.com/items?itemName=256hz.swap-ternary) - or search the VS Code Extension Marketplace for `SwapTernary`.  You can also [check out the repo](https://github.com/256hz/SwapTernary) if you just want to see the code.

By my calculations, if I save only 11,000 developers just five seconds each, it will be worth the time investment!  Demos below.

---

![preserves formatting](https://j.gifs.com/oV5V2L.gif)
![supports nested ternaries (not that you should)](https://j.gifs.com/JyoyZK.gif)

# Development

## Getting Started

Fortunately for us, the VS Code team has made bootstrapping a project a pretty quick process.  One thing I will say for Microsoft is that their push towards better documentation really shines in some areas.  This is one of them.  

### Please Stop Working With ICE

A much larger thing I will say against Microsoft - [stop working with ICE](https://www.vox.com/recode/2019/10/9/20906605/github-ice-contract-immigration-ice-dan-friedman).

## Getting Started, cont'd

Their doc [Your First Extension](https://code.visualstudio.com/api/get-started/your-first-extension) was hugely helpful, and I recommend following it closely.  I'll cover the essentials here.

---

[![Boots by Oziel Gomez on Unsplash](https://images.unsplash.com/photo-1511283402428-355853756676?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80)](https://unsplash.com/photos/x7gz40Z9ObM)

## Bootstrap Your Extension

The team have made an NPM package using [yeoman](https://yeoman.io/) that generates a Hello World extension for you, complete with tests.  You can install it with the following command in Terminal (assuming you use NPM):

```bash
npm i -g yo generator-code
```

When that's all set, run `yo code` from the folder you'd like to have your code in.  It offers a nice selection of starter templates:

```
❯ New Extension (TypeScript) 
  New Extension (JavaScript) 
  New Color Theme 
  New Language Support 
  New Code Snippets 
  New Keymap 
  New Extension Pack 
  New Language Pack (Localization) 
```

I chose the first option.  You'll also be able to enter a name, description, initialize a repo, etc. (Hit Enter to go with defaults - you can always change stuff later.)

Once that finishes installing, open up `src/extension.ts` in Code.  Hit F5 to start the debugger.  A new Code window with your extension installed should appear.

Then you can enter the Command Palette (Mac: ⇧⌘P, Windows: ^⇧P) and type Hello World, or whatever you named it in the last step.  You should be able to hit Enter and see a friendly popup.

---

[![Editing by Kelly Sicema on Unsplash](https://images.unsplash.com/photo-1515787366009-7cbdd2dc587b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80)](https://unsplash.com/photos/N3o-leQyFsI)

## Let's Write Some Code

To swap a ternary, I knew I would need to edit selected text.  The team have graciously compiled a [repo with ~50 sample extensions](https://github.com/microsoft/vscode-extension-samples) for us to work from.  How about `document-editing-sample`?  That seems promising.  For me, it's perfect - an extension that reverses the selected text.  The whole thing is below for reference.

```ts
// extension.ts
export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('extension.reverseWord', function () {
    // Get the active text editor
    const editor = vscode.window.activeTextEditor;

    if (editor) {
      const { document, selection } = editor;

      // Get the word within the selection
      const word = document.getText(selection);
      const reversed = word.split('').reverse().join('');
      editor.edit(editBuilder => {
        editBuilder.replace(selection, reversed);
      });
    }
  });

  context.subscriptions.push(disposable);
}
```

There's a lot of useful info here.

1. Your extension must be wrapped in a function called `activate`, which takes the editor context as an argument.
1. Your code must be registered by the `registerCommand` command and stored as a variable.
1. That variable must be pushed to the `context.subscriptions` array, which where VS Code manages active extensions.
1. You already have all this boilerplate in your new extension.
1. `registerCommand` is where we come in.  It takes two arguments: a string and a function.  Let's talk about the string first.

---

[![Phone by Quino Al on Unsplash](https://images.unsplash.com/photo-1525598912003-663126343e1f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2850&q=80)](https://unsplash.com/photos/8gWEAAXJjtI)

## The Command String

The *command string* takes the format of `<publisher>.<commandName>`.  Mine is `256hz.swapTernary`.

The publisher is you.  If you don't have an Azure DevOps account yet, don't change it now; we'll cover creating a publisher in the next post.

Enter a `commandName`.  Use simple, letter-only strings.  Now, add the whole command string into your `package.json` twice: under `activationEvents` and `contributes.commands`.

```json
// package.json
...
  "activationEvents": [
    "onCommand:256hz.swapTernary"
  ],
...
  "contributes": {
    "commands": [
      {
        "command": "256hz.swapTernary",
        "title": "Swap Ternary"
      }
    ]
  },
```

Make sure the command string is the same in all three places or nothing will work.  This is especially true if you are like me and change the name of your command 50 times.

The `title` field above is the friendly name that will show up in the Command Palette.

(You can also add a keybinding here.  See [my package.json](https://github.com/256hz/SwapTernary/blob/main/package.json) for an example.)

---

[![Blueprints by Sven Mieke on Unsplash](https://images.unsplash.com/photo-1542621334-a254cf47733d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80)](https://unsplash.com/photos/fteR0e2BzKo)

## Your Custom Function

Now, the `fun` part.  Er, the function.  This is the entry point for your command.

```ts
    const editor = vscode.window.activeTextEditor;
```

This gives us access to the active editor environment.

```ts
    if (editor) {
      const { document, selection } = editor;
```

Our editor context has a `document` and a `selection`.  Cool.  These are not raw text, by the way - they are instances of the  `Document` and `Selection` classes.  Each has special methods and properties.

```ts
      const word = document.getText(selection);
```

This is how we get our raw text.  The `Selection` contains pointers to its beginning & end positions in the `Document`, and the document takes these and gives you what's between them.

You can perform whatever logic you want on the text now, and all the work in my extension takes place at this step.  In the example, they reverse the text.

```ts
      const reversed = word.split('').reverse().join('');
```

Then, we use the active editor's `edit` method to replace what we have.  `edit` takes a callback and passes in something called the `editBuilder`.

```ts
      editor.edit(editBuilder => {
        editBuilder.replace(selection, reversed);
      });
```

`editBuilder.replace` takes two arguments: a `Selection`, and  our new, raw text.  `selection` is already good to go - we destructured it out of the `editor` above.  So all you have to do now is process your text and pass it in as the second argument.

I needed to do a bunch of string manipulation for my extension:

- split the statement up into the condition, true/false clauses, and formatting. 
- keep track of whether you're in the middle of a string, so the `?:` characters won't be wrongly parsed as syntax.
- keep track of nested ternaries, so we only swap the outermost expression.
- show error modals if the expression can't parse properly.

Happy to answer questions about those if you're interested, or you can [check out the repo](https://github.com/256hz/SwapTernary).

That's all for development!  Now, let's publish the extension.  On to part 2.

---

Cover photo: [Code by Clement H on Unsplash](https://unsplash.com/photos/95YRwf6CNw8)