This is part two, deploying a Visual Studio Code touchbar extension for Markdown notes, using the normal “correct” development method. I’d suggest referring to part one, for my much simpler, zero-code method.
Touchbar
For a change this time, I am not using any icons, but instead using Unicode text characters, symbols and emojis!
Important: this is a tutorial to make and customize your own extension. It is not available in the VS Code Marketplace. The reason is this: if you want to change the touchbar items, you need to manually one of the source files!
Pre-requisites
I’m not going into details this time, just note you need:
- Markdown Extended extension
- ideally, the setting
keyboard.touchbar.ignored
setup to maximize the usable touchbar space, in either insettings.json
or your workspace’s.vscode/settings.json
:
"keyboard.touchbar.ignored": [
"workbench.action.navigateBack",
"workbench.action.navigateForward",
"workbench.action.debug.start",
"workbench.action.debug.run"
]
- this time, I am not using icons, so you can skip getting and editing the PNG icons.
Creating the custom Touchbar Extension
The shortcut method was manually creating a few files in ~/.vscode/extensions
- this time at minimum you need extension.js
and package.json
.
However, the correct way to create an extension is to install Yeoman and run the VS Code Extension Generator. I do it in a Docker container:
- Setup a shared Docker folder and run
docker run --rm -it -v <folder>:/home/node node bash
- In the container, run:
npm install -g yo generator-code su node cd ~ yo code
- Fill in the details similar to below:
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? New Extension (JavaScript)
? What's the name of your extension? MyByways Touchbar
? What's the identifier of your extension? mybyways-touchbar
? What's the description of your extension?
? Enable JavaScript type checking in 'jsconfig.json'? No
? Initialize a git repository? No
? Which package manager to use? npm
Writing in /home/node/mybyways-touchbar...
...
Your extension mybyways-touchbar has been created!
- And... the source files will be created. You can exit the Docker container now (
exit
twice). - Edit
extension.js
andpackage.json
- source code for both are further down. - At this stage, you can actually open the Workspace and debug the extension in VS Code!
- But, to deploy the extension so that it the extension automatically when VS Code starts, first create a new folder for the extension and then copy all files into it, e.g. create
~/.vscode/extensions/mybyways.touchbar-1.0.0/
if you want to follow the standard naming convention. - Now you can re-start VS Code to test.
Anatomy of a Touchbar Extension
There are only 2 mandatory files:
- an Extension Manifest
package.json
, - the source code for the extension,
extension.js
The manifest contains every parameter to configure the touchbar and commands:
- the source entry point i.e.
main
points toextension.js
, - activationEvents defines all events that activate the extension,
- contributes.commands defines all commands that extension contributes to VS code,
- contributes.configuration defines a list of built-in or extension commands that the touchbar items will trigger,
- and contributes.menus.touchBar which are the items (buttons) are shown in the touchbar.
When the extension is first run, VS code reads contributes.menus.touchBar
and displays the touchbar items (button). The first time any touchbar item (button) is pressed, VS code triggers an onCommand
event as in the activationEvents
section to activate the extension. This should happen only once.
Now the code in the activate()
function in extension.js
gets executed, to register (binds) all the extension’s new commands (defined in the contributes.commands
section and prefixed with mybyways-touchbar
) to functions. In all cases, the functions will simply execute another command, and this is specified in the contributes.configuration
section of the manifest. Only now can the command associated with the touchbar item be triggered.
As an example, assuming this is the first touchbar item pressed: pressing "B" for bold triggers onCommand:mybyways-touchbar.toggleBold
, which triggers activate()
, which reads contributes.configuration
and registers all commands including the mybyways-touchbar.toggleBold
command, which in turn executes the existing command markdownExtended.toggleBold
.
package.json
The Extension Manifest, at minimum (read: un-publishable), contains:
{
"name": "mybyways-touchbar",
"displayName": "MyByways Touchbar",
"publisher": "MyByways.com",
"version": "0.0.1",
"engines": { "vscode": "^1.50.0" },
"main": "./extension.js",
"activationEvents": [
"onCommand:mybyways-touchbar.toggleBold",
"onCommand:mybyways-touchbar.toggleItalics",
"onCommand:mybyways-touchbar.toggleUList",
"onCommand:mybyways-touchbar.toggleOList",
"onCommand:mybyways-touchbar.toggleMark",
"onCommand:mybyways-touchbar.toggleStrikethrough",
"onCommand:mybyways-touchbar.toggleUnderline",
"onCommand:mybyways-touchbar.toggleSuperscript",
"onCommand:mybyways-touchbar.toggleSubscript",
"onCommand:mybyways-touchbar.indentLines",
"onCommand:mybyways-touchbar.outdentLines",
"onCommand:mybyways-touchbar.nextMatchFindAction",
"onCommand:mybyways-touchbar.previousMatchFindAction",
"onCommand:mybyways-touchbar.toggleMinimap",
"onCommand:mybyways-touchbar.toggleWordWrap",
"onCommand:mybyways-touchbar.quickFix",
"onCommand:mybyways-touchbar.showCommands",
"onCommand:mybyways-touchbar.togglePanel",
"onCommand:mybyways-touchbar.toggleSidebarVisibility",
"onCommand:mybyways-touchbar.files.newUntitledFile",
"onCommand:mybyways-touchbar.editor.changeLanguageMode"
],
"contributes": {
"commands": [
{ "title": "𝐁", "command": "mybyways-touchbar.toggleBold" },
{ "title": "𝑖", "command": "mybyways-touchbar.toggleItalics" },
{ "title": "•", "command": "mybyways-touchbar.toggleUList" },
{ "title": "⒈", "command": "mybyways-touchbar.toggleOList" },
{ "title": "=", "command": "mybyways-touchbar.toggleMark" },
{ "title": "–", "command": "mybyways-touchbar.toggleStrikethrough" },
{ "title": "_", "command": "mybyways-touchbar.toggleUnderline" },
{ "title": "¹", "command": "mybyways-touchbar.toggleSuperscript" },
{ "title": "₂", "command": "mybyways-touchbar.toggleSubscript" },
{ "title": "⇥", "command": "mybyways-touchbar.indentLines" },
{ "title": "⇤", "command": "mybyways-touchbar.outdentLines" },
{ "title": "🔎", "command": "mybyways-touchbar.nextMatchFindAction" },
{ "title": "🔍", "command": "mybyways-touchbar.previousMatchFindAction" },
{ "title": "◨", "command": "mybyways-touchbar.toggleMinimap" },
{ "title": "↩", "command": "mybyways-touchbar.toggleWordWrap" },
{ "title": "⎀", "command": "mybyways-touchbar.quickFix" },
{ "title": ">_", "command": "mybyways-touchbar.showCommands" },
{ "title": "⬓", "command": "mybyways-touchbar.togglePanel" },
{ "title": "◧", "command": "mybyways-touchbar.toggleSidebarVisibility" },
{ "title": "New", "command": "mybyways-touchbar.files.newUntitledFile" },
{ "title": "✍︎", "command": "mybyways-touchbar.editor.changeLanguageMode" }
],
"configuration": {
"title": "MyByways Touchbar",
"properties": {
"mybyways-touchbar.commands": {
"type": "object",
"description": "Command ID Prefixes and Names",
"default": {
"markdownExtended": ["toggleBold", "toggleItalics", "toggleUList", "toggleOList", "toggleMark", "toggleStrikethrough", "toggleUnderline", "toggleSuperscript", "toggleSubscript"],
"workbench.action": ["showCommands", "togglePanel", "toggleSidebarVisibility","files.newUntitledFile","editor.changeLanguageMode"],
"editor.action": ["indentLines", "outdentLines", "nextMatchFindAction","previousMatchFindAction","toggleMinimap","toggleWordWrap","quickFix"]
}
}
}
},
"menus": {
"touchBar": [
{ "command": "mybyways-touchbar.toggleBold", "group": "a@1", "when":"editorLangId==markdown" },
{ "command": "mybyways-touchbar.toggleItalics", "group": "a@2", "when":"editorLangId==markdown" },
{ "command": "mybyways-touchbar.toggleMark", "group": "a@3", "when":"editorLangId==markdown" },
{ "command": "mybyways-touchbar.toggleUList", "group": "a@4", "when":"editorLangId==markdown" },
{ "command": "mybyways-touchbar.toggleOList", "group": "a@5", "when":"editorLangId==markdown" },
{ "command": "mybyways-touchbar.showCommands", "group": "b@1"},
{ "command": "mybyways-touchbar.togglePanel", "group": "b@2" },
{ "command": "mybyways-touchbar.files.newUntitledFile", "group": "b@3", "when": "!editorIsOpen" },
{ "command": "mybyways-touchbar.nextMatchFindAction", "group": "b@4", "when": "editorIsOpen" },
{ "command": "mybyways-touchbar.previousMatchFindAction", "group": "b@5", "when": "editorIsOpen&&editorLangId!=markdown"},
{ "command": "mybyways-touchbar.editor.changeLanguageMode", "group": "b@6", "when": "editorLangId==plaintext" },
{ "command": "mybyways-touchbar.quickFix", "group": "c@1", "when": "editorHasCodeActionsProvider" },
{ "command": "mybyways-touchbar.indentLines", "group": "c@2", "when": "editorIsOpen" },
{ "command": "mybyways-touchbar.toggleWordWrap", "group": "c@3", "when": "editorIsOpen" }
]
}
}
}
Activation Events
The only activation event required is
onCommand
. All commands
actually shown in the touchbar via the contributes.menu.touchbar
configuration must have a corresponding onCommand
event.
Commands
The commands section contains any (and all) items that could appear in the touchbar. What I show above is a superset of some items that are relevant to me. These can be built-in commands e.g. editor.action.*
or commands contributed by extensions like markdownExtended.*
.
Each command
comprises an icon, a title and a command ID:
{
"title": "𝐁",
"command": "mybyways-touchbar.toggleBold"
},
To add more commands, simply add a line with the command title (unicode characters work fine) and command ID. Here’s a tip to determine an existing command ID:
- open Code > Preferences > Keyboard Shortcuts
- search for a command, e.g. "bold" and once found...
- right-click on the item and Copy Command ID.
If you were to rename the title of an existing command, e.g. { "title": "𝐁", "command": "mybyways-touchbar.toggleBold" }
, then you would end up with this in the Command Palette and Keyboard Shortcuts UI - this is bad because you can no longer search for the original command title:
Configuration
Let me start of by saying there are no user configurable settings! I use the configuration section merely so that I do not have to ever edit extension.js
source code even when adding new command IDs. Although you can provide overrides in settings.json
, why bother, since you will have to edit the package.json
manifest anyway.
In essence, I want to register a new command e.g. mybyways-touchbar.toggleBold
which runs an existing command, e.g. markdownExtended.toggleBold
. Another example is mybyways-touchbar.showCommands
which runs the existing command workbench.action.showCommands
. Notice that the last part of the command IDs must match, so the code shown later can just read this configuration and figure out the rest.
This is where the configuration resides, in a default
section (truncated for readability), so change this part to add command IDs, you should be able to work out what goes where (note the arrays):
"default": {
"markdownExtended": [ "toggleBold", "toggleItalics", "toggleUList", ...],
"workbench.action": [ "showCommands", "togglePanel", "toggleSidebarVisibility", ...],
"editor.action": [ "indentLines", "outdentLines", "nextMatchFindAction", ...]
}
Menus
Finally, the menus.touchbar section specifies what items appear in the touchbar, and when.
Each touchbar item comprises:
- a command ID, which much match a command in the previous section,
- a
group
that breaks up the touchbar items into sections- by default, touchbar items are sorted by the command title - to change the order use the
group
attribute with thea@1
,a@2
,a@3
... notation. - touchbar groups are also sorted by the group name, hence, I start mine with
a
,b
,c
... so that the markdown group nameda@*
appears beforeb@*
, etc.
- by default, touchbar items are sorted by the command title - to change the order use the
- and optionally,
when
which is the condition wherein the touchbar item is visible i.e. visiblewhen
the document language is Markdown:
{
"command": "mybyways-touchbar.toggleBold",
"group": "a@1",
"when":"editorLangId==markdown"
},
On the touchbar on a 16" MacBook Pro, I can display only about 11 items, with the keyboard.touchbar.ignored
setting, depending on the width of team item (text or icon) and the number of groups. If the items exceed the space available on the touchbar, then the entire group will not be shown. You have to experiment.
You may be interested to know the standard two groups are named navigation
and 9_debug
- see the VS Code source code, editor.contribution.ts and debug.contribution.ts respectively.
extension.js
In the commands
section, each new command added by this extension will simply execute an existing commands. You can trigger any built-in or extension commands. For example:
let c = vscode.commands.registerCommand('mybyways-touchbar.toggleBold', function() {
vscode.commands.executeCommand('markdownExtended.toggleBold');
});
context.subscriptions.push(c);
As explained, I want to avoid having to change any source code when adding new commands, preferring to make any changes in the Extension Manifest. To do this, the code reads the configuration section using getConfiguration()
and then does registerCommand()
for each new command added. Pardon the un-readable code:
const vscode = require('vscode');
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
const cmd = vscode.commands,
sub = context.subscriptions,
cfg = vscode.workspace.getConfiguration('mybyways-touchbar.commands');
Object.keys(cfg).forEach(key=>{
const val = cfg[key];
if (Array.isArray(val)) val.forEach(itm=>{
sub.push(cmd.registerCommand("mybyways-touchbar." + itm, ()=>{
cmd.executeCommand(key + "." + itm)
}))
})
})
}
function deactivate() {}
module.exports = { activate, deactivate }
launch.json
If you create these files manually, and want to run and debug the extension in VS Code, then you will need to create one more file. .vscode/launch.json
. This tells VS Code to run it as an extension. You need to open the folder as a Workspace:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [ "--extensionDevelopmentPath=${workspaceFolder}" ]
}
]
}
Conclusion
To-recap:
- you don’t really need node or npm installed, running them from a Docker container works just fine,
- you can run and debug the extension from VS Code, nothing else needed,
- to add a new command to an extension - add it to the Configuration (
contributes.configuration
) and Commands (contributes.commands
) sections. - to add a new item to the touchbar: add it to the Menus (
contributes.menus.touchBar
) section and make sure there is also anonCommand
in the Activation Events (activationEvents
) section. - using a default configuration, there should be no need to edit the code in
extension.js
- I use a couple of tricks to parse the entire JSON configuration section.
I’d say this is the officially “correct” way to create a touchbar extension, and customize it to meet your needs. But why bother when my shortcut method works so well!
Alas, I have no intention of ever creating a proper Marketplace extension. Good luck!
Credit: The Nasc VSCode Touchbar inspired me to investigate my own improved version!