DEV Community

Cover image for How to Build a VS Code extension for Markdown preview using Remark processor
Laura Lindeman for Salesforce Engineering

Posted on

How to Build a VS Code extension for Markdown preview using Remark processor

Add missing remark preview support by writing your own extension.

Author:

Subrat Thakur (subrat.thakur@salesforce.com)
LinkedIn: https://www.linkedin.com/in/subrat-thakur/


Introduction

Visual Studio Code (VS Code) is a cross-platform, light-weight code editor created by Microsoft for Linux, Windows, and macOS. VS Code has support for extensions to add extra capabilities. If a feature you want is missing, you can create an extension to add it.

In my case, I wanted to be able to use a Markdown processor other than the one natively supported by VS Code. VS Code supports Markdown files out of the box and targets the CommonMark Markdown specification using the markdown-it library. But there is no way to use another markdown processor like remark, common marker, or showdown instead of markdown-it in default preview. You can definitely change the style by adding CSS to update the look and feel but the internal processor will stay as markdown-it. So, I decided to create my first VS Code extension! In addition to sharing this specific extension in the Marketplace, I wanted to outline the steps I took to build it, so that you can build an extension for a feature you want to see.

Markdown Previewer

The very first step of building a VS Code Extension

To get started, install the following if you haven’t already:

  1. VS Code
  2. Node.js
  3. Yeoman: An open-source client-side scaffolding tool that helps you kickstart new projects.
  4. vscode-generator-code: A Yeoman generator to generate a new Visual Studio Code extension project from a template.

To install Yeoman and vscode-generator-code: npm install -g yo generator-code

VS Code extensions support two main languages: JavaScript and TypeScript. So having basic knowledge of either of these is pretty mandatory.

Generate a VS Code Extension Template

Go To ‘Terminal’ (‘Command Prompt’ in case of Windows), navigate to the folder where you want to generate the extension, type the following command, and hit Enter:

yo code

At the prompt, you must answer some questions about your extension:

  • What type of extension do you want to create? New Extension (TypeScript).

  • What’s the name of your extension? (in my case it was Remark Preview)

  • What’s the identifier of your extension? (consider it Visible Name in small-case separated by -) e.g remark-preview

  • What’s the description of your extension? Write something about your extension (you can fill this in or edit it later too).

  • Initialize a Git repository? This initializes a Git repository, and you can add set-remotelater. To create an extension Git repo is not a mandatory thing. You can publish it directly from your local as well.

  • Which package manager to use? You can choose yarn or npm; I will use npm.

Hit the Enter key, and it will start installing the required dependencies. And finally you will get:

Your extension remark-preview has been created!

To start editing with Visual Studio Code, use the following commands:

 cd remark-preview
 code .
Enter fullscreen mode Exit fullscreen mode

The project's structure

Once you are done with generating the extension template and open it in VS Code, your editor will look like this:
Preview of editor at this stage
The generator-code project creates the most basic wiring and plumbing needed for a functioning extension. If you look in the extension directory, you’ll see several files:

  • vsc-extension-quickstart.md provides some instructions for creating and using the extension.
  • extension.js is the actual code for the extension. The entire extension doesn’t need to fit into this one file, but this is the default entry point for the extension.
  • jsconfig.json controls how the project’s JavaScript code is handled by the Node.js runtime. You generally don’t need to change anything here.
  • package.json contains the packaging information for your extension. If you are a Node.js developer, some of this might look familiar since name, description, version, and scriptsare common parts of a Node.js project.

I will talk about the files more, but, for now, let’s try running our basic extension.

// This will install all the dependencies
npm install

// This will compile your code 
npm run compile

// Run the code by hitting f5
f5
Enter fullscreen mode Exit fullscreen mode

The above code instructions will open a new VS Code window with your basic extension installed in it.
VS Code window with basic extension installed

What’s different inside Package.json ?

  • There are a few sections that are very important.
    • engines: States which version of VSCodium the extension will support
    • categories: Sets the extension type; you can choose from Languages, Snippets, Linters, Themes, Debuggers, Formatters, Keymaps, and Other
    • contributes: A list of commands that can be used to run with your extension
    • main: The entry point of your extension
    • activationEvents: Specifies when the activation event happens. Specifically, this dictates when the extension will be loaded into your editor. Extensions are lazy-loaded, so they aren't activated until an activation event occurs

Section of code displaying activation events
Let’s focus on few concepts that are crucial for VS Code extension development:

  • Activation Events: events upon which your extension becomes active. This will be part of package.json e.g
//the extension becomes activated when user runs the `Hello World` command
"activationEvents": [
  "onCommand:opendocs.helloWorld"
],
Enter fullscreen mode Exit fullscreen mode

This is currently there inside your newly generated projects package.json

A few more example would be:
EX 1:

//This activation event is emitted and interested extensions will be activated 
// whenever a file that resolves to a certain language gets opened.

"activationEvents": [
  "onLanguage:markdown"
]
Enter fullscreen mode Exit fullscreen mode

As we are planning to develop an extension for markdown, the above could be suitable for our extension ⬆️

EX 2:

// This activation event is emitted and interested extensions will be activated 
// whenever a folder is opened and the folder contains at least one file 
// that matches a glob pattern. 
"activationEvents": [
  "workspacecontains: package.json"
]
Enter fullscreen mode Exit fullscreen mode

EX 3:

//The `*` activation event is emitted and interested extensions will be activated 
// whenever VS Code starts up.
"activationEvents": [
  "*"
]
Enter fullscreen mode Exit fullscreen mode

You can also provide multiple multiple event as "activationEvents" is an array.

Contribution Points

Static declarations that you make in the package.json Extension Manifest to extend VS Code. This could be used to add commands available in command pallets, menu bar, editor title, editor context as well as configurations related to the extension.

  • Commands: Contribute the UI for a command consisting of a title and (optionally) an icon, category, and enabled state.
  • Configuration: Contribute configuration keys that will be exposed to the user. The user will be able to set these configuration options as User Settings or as Workspace Settings, either by using the Settings UI or by editing the JSON settings file directly.
  • menus: Contribute a menu item for a command to the editor or Explorer. The menu item definition contains the command that should be invoked when selected and the condition under which the item should show.
  • keybindings: Defining key binding for window (linux) and mac for each command.
  • Command Palette: When registering commands in package.json, they will automatically be shown in the Command Palette (⇧⌘P). To allow more control over command visibility, there is the command palette menu item. It allows you to define a when condition to control if a command should be visible in the Command Palette or not.

  • Sorting of groups: Menu items can be sorted into groups. The order inside a group depends on the title or an order-attribute. The group-local order of a menu item is specified by appending @<number> to the group identifier

Registering a command

vscode.commands.registerCommand binds a command id to a handler function in your extension. This will be part of extension.js by default (or whatever entry point you mentioned in package.json).

Once you are done adding these few points your extension will be in good shape in terms of configuration. Let’s talk about our goal to generate a preview.

Generating Preview

We already decided that we would use remark processor to process our markdown, but the most important question is: how are we going to show it inside VS Code?

So the one word answer for it is Webview.

What is Webview ?

As per VS Code documentation, the webview API allows extensions to create fully customizable views within Visual Studio Code. For example, the built-in Markdown extension uses webviews to render Markdown previews. Think of a webview as an iframe within VS Code that your extension controls. A webview can render almost any HTML content in this frame, and it communicates with extensions using message passing.

So the flow is going to be very simple :

  1. Create HTML of our Markdown content using remark.
  2. Add it to Webview as content.
  3. Show that Webview in VS Code!

Simple, right?! I promise it is simpler than it sounds!

SO, let’s begin.

Create HTML content from Markdown content

This code sample will turn markdown into HTML. In our case, instead of creating another Markdown compiler instance, we will use @sfdocs-internal/compiler package .

import admonitions from 'remark-admonitions';
import * as html from 'remark-html';
import * as remark from 'remark';

function markdownCompiler(): any {
    const admonitionsOptions = {};

    return remark()
        .use(html)
        .use(admonitions, admonitionsOptions);
}

let currentHTMLContent = await markdownCompiler().process(markdownContent);
Enter fullscreen mode Exit fullscreen mode

You need to add the below entries to package.json and run npm install to install new dependencies:

        "remark": "12.0.1",
        "remark-admonitions": "1.2.1",
        "remark-html": "12.0.0",
Enter fullscreen mode Exit fullscreen mode

Create a Webview Panel

// Create and show a new webview
this.panel = vscode.window.createWebviewPanel(
        // Webview id
        'liveHTMLPreviewer',
        // Webview title
        '[Preview] ' + fileName,
        // This will open the second column for preview inside editor
        2,
        {
            // Enable scripts in the webview
            enableScripts: true,
            retainContextWhenHidden: true,
            // And restrict the webview to only loading content from our extension's `assets` directory.
            localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'assets'))]
        }
 );
Enter fullscreen mode Exit fullscreen mode

Add HTML to Webview Content

this.panel.webview.html = await markdownCompiler().process(markdownContent);
Enter fullscreen mode Exit fullscreen mode

Finally, this is what your extension.ts should look like:

import * as vscode from 'vscode';
import { admonitions } from 'remark-admonitions';
import * as html from 'remark-html';
import * as remark from 'remark';
import * as path from 'path';


export function activate(context: vscode.ExtensionContext) {

    // commandId
    const SIDE_PREVIEW_COMMAND = 'remark.sidePreview';

    const disposableSidePreview = vscode.commands.registerCommand(SIDE_PREVIEW_COMMAND, async () => {
        initMarkdownPreview(context);
    });

    context.subscriptions.push(disposableSidePreview);
}

async function initMarkdownPreview(context: vscode.ExtensionContext) {
    const panel = vscode.window.createWebviewPanel(
        // Webview id
        'liveHTMLPreviewer',
        // Webview title
        '[Preview]',
        // This will open the second column for preview inside editor
        2,
        {
            // Enable scripts in the webview
            enableScripts: true,
            retainContextWhenHidden: true,
            // And restrict the webview to only loading content from our extension's `assets` directory.
            localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'assets'))]
        }
    );
    panel.webview.html = await markdownCompiler().process(vscode.window.activeTextEditor?.document.getText());
}

function markdownCompiler(): any {
    const admonitionsOptions = {};
    return remark()
        .use(html)
        .use(admonitions, admonitionsOptions);
}



// this method is called when your extension is deactivated
export function deactivate() { }
Enter fullscreen mode Exit fullscreen mode

You can make it more modular and efficient.

Now let’s talk about few more things that might be useful for your new preview extension.


Developer Tools for Webview

As I already told you, Webview is very similar to a Web Browser; you can actually inspect the Webview Panel, check the console logs, and debug any script added to it when the Webview is open. To do all this, open Command Palette (⇧⌘P) and Search for “Developer: Open Webview Developer Tools.” This will open a developer console very similar to Chrome browser.

Content security policy for Webview

Content security policies further restrict the content that can be loaded and executed in webviews.

To add a content security policy, put a <meta http-equiv="Content-Security-Policy"> directive at the top of the webview's <head>.

Here's a content security policy that allows loading local scripts and stylesheets (inline as well), and loading images over https:

<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${panel.webview.cspSource} 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${panel.webview.cspSource} 'self' 'unsafe-inline'; font-src ${panel.webview.cspSource}">
Enter fullscreen mode Exit fullscreen mode

The ${webview.cspSource} value is a placeholder for a value that comes from the webview object itself.

We can also load the scripts and css in old fashion way if we use vscode uri path instead of relative path.

function **getDynamicContentPath**(context, panel, filepath) {
    const onDiskPath = vscode.Uri.file(path.join(context.extensionPath, filepath))
    const styleSrc = panel.webview.asWebviewUri(onDiskPath);
    return styleSrc
}
Enter fullscreen mode Exit fullscreen mode

VS Code creates a secure URI for webview panel and makes it accessible for Iframe.

<link rel="stylesheet" type="text/css" href="${getDynamicContentPath(context, panel, 'assets/css/admonitions.css')}">
Enter fullscreen mode Exit fullscreen mode

How to interact with Webview through Messages?

As we all know by now, Webview is content inside an iframe, so we need special messaging to send events to Webview.
So whenever we want to send a message we can use something similar to:

private postMessage(msg: any) {
        // here this flag is to check if webview panel is still alive or not
        if (!this._disposed) {

        // this.panel is the instance of Webview Panel
            this.panel.webview.postMessage({
                        type: 'messageFromExtension',
                        line: visibleRanges});
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now let’s see how to catch this message inside Webview. The most important thing is that this script should be part of the HTML that we add to Webview panel.

window.addEventListener('message', event => {
    const message = event.data;
    if (message.type === 'messageFromExtension') {
        console.log('Line : ' + message.line[0][0].line);
        onUpdateView(per);
    }
});
Enter fullscreen mode Exit fullscreen mode

In a very similar fashion, we can catch any window event of Webview as well like scroll, click, double click, etc.

For example:

window.addEventListener('scroll', throttle(() => {
    console.log("Scroll Event in Webview window");
}, 100));
Enter fullscreen mode Exit fullscreen mode

To send any message back to VS Code, we need to use acquireVsCodeApi.

// acquireVsCodeApi is the api to talk to extension from webview
const vscode = acquireVsCodeApi();

// createPosterForVsCode is a class provided by VSCode
const messaging = createPosterForVsCode(vscode);
messaging.postMessage('revealLine', { line });
Enter fullscreen mode Exit fullscreen mode

And in the same way, we need a subscribe method for any events coming from Webview:

//this should be part of extension and no need to load inside webview
this.panel.webview.onDidReceiveMessage(e => {
     console.log(e.body.line);
});
Enter fullscreen mode Exit fullscreen mode

Publishing Extensions

To publish an extension, you need to set up a few things.

  • Install VSCE : vsce, short for "Visual Studio Code Extensions," is a command-line tool for packaging, publishing and managing VS Code extensions. You can use vsce to easily package and publish your extensions. It can also search, retrieve metadata, and unpublish extensions.

To install VSCE: npm install -g vsce

  • Get a Personal Access Token: You need to create an account on Azure Dev. You can log in using Github as well. Once you log in, you have to create a project; you can probably name it VSCode Extensions. Once you are done, let’s move ahead to create Personal Access Token.

    • Open the User settings dropdown menu next to your profile image (top right corner) and select Personal access tokens:
    • User settings dropdown menu indicating where to select Personal access tokens
    • On the Personal Access Tokens page, click New Token to create a new Personal Access Token
    • Give the Personal Access Token a name; optionally, you can extend its expiration date to one year, make it accessible to every organization, or select a custom defined scope ruleset:
    • Options to select when saving your Personal Access Token
    • Finally, scroll down the list of possible scopes until you find Marketplace and select Manage:
    • Managing scopes for your Personal Access Token
    • Select Create and you'll be presented with your newly created Personal Access Token. Copy it; you'll need it to create a publisher.
  • Create a publisher : A publisher is an identity that can publish extensions to the Visual Studio Code Marketplace. Every extension needs to include a publisher name in its package.json file.

Use this command to create publisher : vsce create-publisher

  • Log in to a publisher: This is a one-time task and requires your publisherId and Personal Access Token.

Use this command to login : vsce login

  • Publish your extension: As the final step, publish your extension to marketplace.

Use this command to publish : vsce publish

Keep in mind you always need to change version in package.json before re-publishing the extension, or else it will throw an error. You can use a command like vsce publish minor or vsce publish 2.0.1 .

Wrapping up

One message I want you to take to heart is that creating a VSCode extension is not as hard as it looks. It is pretty simple, actually. Sometimes you just need to push yourself a bit and try things out.

VS Code is awesome 😎 and it’s really amazing how we can extend functionality inside it by developing extensions.
There are many helpful APIs that will help you create the extensions you want to build. The VS Code extension API has many other powerful methods you can use.

If you want to take a look at the whole project the code is available here, which might help for code references (watch out for some messy code though!) and you can also find it in the marketplace.

Reference

VS Code Extension Sample
VS Code Extension DOC
Workbench Extension

Top comments (1)

Collapse
 
shubhadip_bhowmik profile image
Shubhadip Bhowmik

Awesome. So detailed explanation. I will create definately.