icyrock.com
HomePureScript experiments - vscode-ext
2017-Sep-28 19:35
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" }] } } |
These are the important keys:
- scripts has watch target that will be used by VSC for incremental recompilation
- engine and devDependencies include the vscode version that this extension can run on
- main points to the output Javascript file that gets generated from Purescript source
- activationEvents specifies that the extension will be activated when sayHello command is invoked. You can read more about it here.
- contributes contains commands sub-key that tells VSC our extension provides sayHello command
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:
