icyrock.com

Home

PureScript experiments - vscode-ext

2017-Sep-28 19:35
purescript-experimentspurescript

Here's how Purescript can be used to write Visual Studio Code extensions. You can find the source here. It is based on the official VSC Hello World extension tutorial.

Initialize the project as usual:

1
2
3
4
mkdir purescript-vscode-ext
cd purescript-vscode-ext
git init
pulp init

Update the package.json to be similar to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "name": "purescript-vscode-ext",
  "version": "1.0.0",
  "description": "Sample Visual Studio Code extension written in Purescript",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "pulp build",
    "watch": "pulp --watch build"
  },
  "keywords": [],
  "author": "icyrock.com",
  "license": "ISC",
  "publisher": "icyrock.com",
  "engines": {
    "vscode": "^1.1.5"
  },
  "main": "./output/Main/index",
  "devDependencies": {
    "vscode": "^1.1.5"
  },
  "activationEvents": [
    "onCommand:extension.sayHello"
  ],
  "contributes": {
    "commands": [{
      "command": "extension.sayHello",
      "title": "Hello World"
    }]
  }
}
and run npm install.

These are the important keys:

This sample project has a minimal set of VSC API bindings. Let's go through these one by one.

VsCode.purs and Disposable.purs

These just contain the VSCODE effect and Disposable type:

1
2
3
4
5
module VsCode where
 
import Control.Monad.Eff (kind Effect)
 
foreign import data VSCODE :: Effect
1
2
3
module VsCode.Disposable where
 
foreign import data Disposable :: Type

Commands.purs and Commands.js

These two contain the PS / JS pair for registerCommand, which is used to register a callback that VSC will call when the command is activated.

1
2
3
4
5
6
7
8
9
10
11
module VsCode.Commands where
 
import Prelude
import Control.Monad.Eff (Eff)
import VsCode (VSCODE)
import VsCode.Disposable (Disposable)
 
foreign import registerCommand :: forall eff
  .  String
  -> Eff (vscode :: VSCODE | eff) Unit
  -> Eff (vscode :: VSCODE | eff) Disposable
1
2
3
4
5
6
7
8
9
10
11
const vscode = require('vscode')
 
exports.registerCommand =
  function(command) {
    return function(callback) {
      return function() {
        const disposable = vscode.commands.registerCommand(command, callback)
        return disposable
      }
    }
  }

ExtensionContext.purs and ExtensionContext.js

These two contain the PS / JS pair for subscriptions, which is used to get VSC's subscriptions.

1
2
3
4
5
6
7
8
9
10
11
module VsCode.ExtensionContext where
 
import Control.Monad.Eff (Eff)
import VsCode (VSCODE)
import VsCode.ExtensionContext.Subscriptions (Subscriptions)
 
foreign import data ExtensionContext :: Type
 
foreign import subscriptions :: forall eff
  .  ExtensionContext
  -> Eff (vscode :: VSCODE | eff) Subscriptions
1
2
3
4
5
6
exports.subscriptions =
  function(extensionContext) {
    return function() {
      return extensionContext.subscriptions
    }
  }

Subscriptions.purs and Subscriptions.js

These two contain the PS / JS pair for push, which is used to actually push to VSC's subscriptions.

1
2
3
4
5
6
7
8
9
10
11
12
module VsCode.ExtensionContext.Subscriptions where
 
import Control.Monad.Eff (Eff)
import VsCode (VSCODE)
import VsCode.Disposable (Disposable)
 
foreign import data Subscriptions :: Type
 
foreign import push :: forall eff
  .  Subscriptions
  -> Disposable
  -> Eff (vscode :: VSCODE | eff) Subscriptions
1
2
3
4
5
6
7
8
9
exports.push =
  function(subscriptions) {
    return function(disposable) {
      return function() {
        subscriptions.push(disposable)
        return subscriptions
      }
    }
  }

Window.purs and Window.js

These two contain the PS / JS pair for showInformationMessage. We'll use it to show a sample informational message when our command is activated.

1
2
3
4
5
6
7
8
9
module VsCode.Window where
 
import Prelude
import Control.Monad.Eff (Eff)
import VsCode (VSCODE)
 
foreign import showInformationMessage :: forall eff
  .  String
  -> Eff (vscode :: VSCODE | eff) Unit
1
2
3
4
5
6
7
8
const vscode = require('vscode')
 
exports.showInformationMessage =
  function(message) {
    return function() {
      vscode.window.showInformationMessage(message)
    }
  }

Main.purs

Finally, this is the starting point of the extension. It has two main functions: activate and deactivate. These are going to be called when our extension is activated and deactivated. Both log the fact they have been called, which is visible in VSC's Debug Console. activate also makes a command which will show "Hello World!" message and pushes it to subscriptions. activate uses unsafePerformEff to call activateEff due to Eff being a wrapping function. You can read more about how Eff is represented here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module Main where
 
import Prelude
 
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Eff.Unsafe (unsafePerformEff)
import VsCode (VSCODE)
import VsCode.Commands (registerCommand)
import VsCode.ExtensionContext (ExtensionContext, subscriptions)
import VsCode.ExtensionContext.Subscriptions (push)
import VsCode.Window (showInformationMessage)
 
activateEff :: forall eff
  .  ExtensionContext
  -> Eff ( console :: CONSOLE
         , vscode :: VSCODE
         | eff)
         Unit
activateEff ctx = void do
  log "PureScript extension activated"
  disposable <- registerCommand "extension.sayHello" $
    showInformationMessage "Hello World!"
  ss <- subscriptions ctx
  push ss disposable
 
activate :: ExtensionContext -> Unit
activate ctx = unsafePerformEff $ activateEff ctx
 
deactivate :: forall eff
  . Eff ( console :: CONSOLE
        , vscode :: VSCODE
        | eff)
        Unit
deactivate = do
  log "PureScript extension deactivated"

launch.json and tasks.json

These two reside in .vscode and are used for incremental building and launching an extension host (guest VSC instance). There are instructions to VSC on how to process the npm watch target's output. Specifically, lines specifying the building started, succeeded or failed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "version": "0.1.0",
  "configurations": [
     
    {
      "name": "Launch Extension",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": [
        "--extensionDevelopmentPath=${workspaceRoot}"
      ],
      "stopOnEntry": false,
      "sourceMaps": true,
      "outFiles": [
        "${workspaceRoot}/output/**/*.js"
      ],
      "preLaunchTask": "npm"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "version": "2.0.0",
    "command": "npm",
    "type": "shell",
    "presentation": {
        "reveal": "silent"
    },
    "args": [
        "run",
        "watch"
    ],
    "isBackground": true,
    "problemMatcher": {
        "pattern": {
           "regexp": "\\* ERROR:.*"
        },
        "background": {
            "beginsPattern": "\\* Building project in.*",
            "endsPattern": "\\* Build successful.*"
        }
    }
}

Testing

When the extension project is open, it can be run from within VSC via Debug / Start Debugging menu option. It will launch a separate / guest VSC instance where you can press Ctrl + Shift + P to open the command bar and choose Hello World command:

This will display the message as expected in the guest VSC:

In the Debug Console of the host VSC, you can see the activation (after the command is invoked, triggering the extension to be activated) and deactivation (after the guest VSC is shut down) messages: