ZeroBrane Studio supports several ways to customize its functionality: specification files (spec/) to describe syntax and keywords, apis (api/) to provide code completion and tooltips, and interpreters (interpreters/): to implement components for setting debugging and run-time project environment.

In addition to these options, it also provides plugin API to extend its functionality in a more fine graned way. For example, one can write a plugin to add a menu item to turn line wrapping on or off from the IDE or a plugin to map a shortcut to a character not present on the keyboard.

Plugin repository

There are 60+ extension packages in the package repository, including packages like DocumentMap, UniqueTabName, CloneView, ProjectSettings, SyntaxCheckOnType, and many others.

Plugin structure

A plugin is a Lua file that returns a table with several fields – name, description, author, version – and various event handlers.

For example, the following plugin has only one event handler (onRegister) that is called when the plugin is registered:

return {
  name = "Sample plugin",
  description = "Sample plugin with one event handler.",
  author = "Paul Kulchenko",
  version = 0.1,

  onRegister = function(self) ide:Print("Sample plugin registered") end,
}

The plugin may also return nil instead of the table, which will skip the registeration. This is useful when some environmental conditions are not satisfied (for example, a program that the plugin integrates with is not installed).

Plugin installation

A plugin can be placed in ZBS/packages/ or HOME/.zbstudio/packages folder (where ZBS is the path to ZeroBrane Studio location and HOME is the path specified by the HOME environment variable). The first location allows you to have per-instance plugins, while the second allows to have per-user plugins. The second option may also be preferable for macOS users as the packages/ folder may be overwritten during an application upgrade.

Plugin configuration

Plugins may have its own configuration in the same way as the IDE does. The configuration can be retrieved using GetConfig method, which returns a table with all configuration values. For example, if one of the IDE configuration files includes this assignment pluginname = {value = 1, anothervalue = 2}, then the assigned table will be returned as the result of the GetConfig method.

Plugin data

Plugins may also have its own data, which provides a way to store information between IDE restarts. This may be useful when a plugin stores some information entered by the user (like a registration key) or collects statistics about user actions.

The plugin API provides GetSettings and SetSettings methods that retrieve and save a table with all plugin data elements. For example, the following fragment will increment and save loaded value to keep track of how many times the plugin has been loaded:

return {
  ...

  onRegister = function(self)
    local settings = self:GetSettings()
    settings.loaded = (settings.loaded or 0) + 1
    self:SetSettings(settings)
  end,
}

Plugin guidelines

Plugin authors should only used public API, which includes the following groups of functions:

Plugin dependencies

The plugins may depend on a particular version of ZeroBrane Studio. One of the fields in the plugin description is dependencies that may have as its value (1) a table with various dependencies or (2) a minumum version number of ZeroBrane Studio required to run the plugin.

If the version number for ZeroBrane Studio is larger than the most recent released version (for example, the current release version is 0.50, but the plugin has a dependency on 0.51), this means that it requires a development version currently being worked on (which will become the next release version).

One of the keys in the dependencies table may be osname, which specified what operating system the plugin supports (Windows, Macintosh, and Unix are supported). For example, specifying dependencies = {0.71, osname = "Windows"} sets 0.71 or later as the required version of the IDE and Windows as the supported operating system.

Event handlers

A plugin can register various event handlers. Each handler receives the plugin object as the first parameter and some other parameters depending on the type of the event: onEditor* events get editor object as the second parameter, onMenu* events get menu as the second parameter, onApp* events get application as the second parameter, onProject* get project, and onInterpreter* get interpreter. Events may also get other parameters as documented in the file referenced above.

Registering a specification

Registering a specification as part of a plugin allows you to provide custom syntax highlighting for files with a particular extension. This is done using AddSpec(name, specification) and RemoveSpec(name) methods. For example, the following code will register a specification that is linked to .xml extension:

local spec = {
  exts = {"xml"},
  lexer = wxstc.wxSTC_LEX_XML,
  apitype = "xml",
  stylingbits = 7,
  lexerstyleconvert = {
    text = {wxstc.wxSTC_H_DEFAULT,},
    comment = {wxstc.wxSTC_H_COMMENT,},
    stringeol = {wxstc.wxSTC_HJ_STRINGEOL,},
    number = {wxstc.wxSTC_H_NUMBER,},
    stringtxt = {wxstc.wxSTC_H_DOUBLESTRING,wxstc.wxSTC_H_SINGLESTRING,},
    lexerdef= {wxstc.wxSTC_H_OTHER,wxstc.wxSTC_H_ENTITY,wxstc.wxSTC_H_VALUE,},
    keywords0 = {wxstc.wxSTC_H_TAG,wxstc.wxSTC_H_ATTRIBUTE,},
  },
  keywords = {[[foo bar]]]},
}

return {
  name = "...",
  description = "...",
  author = "...",
  version = 0.1,

  onRegister = function(self)
    -- add specification with name "sample"
    ide:AddSpec("sample", spec)
  end,

  onUnRegister = function(self)
    -- remove specification with name "sample"
    ide:RemoveSpec("sample")
  end,
}

The name of the specification is not critical, but to avoid conflicts with other plugins you may want to include the name of your plugin in your specification name (sample in the example above). This registering of the specification will have the same effect as putting this specification code a file in spec/ folder, but will only be used when the plugin is activated.

Registering an API

Custom API definitions that are used to provide function tooltips and auto-complete functionality can be added by registering an API definition. This is done using AddAPI(group, name, api) and RemoveAPI(group, name) methods. For example, to add sample.val1 and sample.val2 methods, you may use the following:

local api = {
 sample = {
  childs = {
   val1 = {
    description = "Value 1.",
    type = "value"
   },
   val2 = {
    description = "Value 2.",
    type = "value"
   },
  }
 }
}

return {
  name = "...",
  description = "...",
  author = "...",
  version = 0.1,

  onRegister = function(self)
    -- add API with name "sample" and group "lua"
    ide:AddAPI("lua", "sample", api)
  end,

  onUnRegister = function(self)
    -- remove API with name "sample" from group "lua"
    ide:RemoveAPI("lua", "sample")
  end,
}

See the custom API definitions section for details and examples on how to describe complex definitions and the section on registering API definitions to make added definitions recognized by the IDE.

Registering an interpreter

local interpreter = {
  name = "Sample",
  description = "Lua sample interpreter",
  api = {"baselib", "sample"},
  frun = function(self,wfilename,rundebug)
    if rundebug then
      ide:GetDebugger():SetOptions({ --[[ pass debugging options here if needed ]] })
    end
    CommandLineRun(('lua "%s"'):format(wfilename:GetFullPath()),self:fworkdir(wfilename),true,false)
  end,
  hasdebugger = true,
}

return {
  name = "...",
  description = "...",
  author = "...",
  version = 0.1,

  onRegister = function(self)
    -- add interpreter with name "sample"
    ide:AddInterpreter("sample", interpreter)
  end,

  onUnRegister = function(self)
    -- remove interpreter with name "sample"
    ide:RemoveInterpreter("sample")
  end,
}

Registering a console alias

A console alias allows adding commands to the local console in the IDE. This is done using AddConsoleAlias(name, commands) and RemoveConsoleAlias(name) methods. For example, to add install and uninstall commands to the console, you may do the following:

local commands = {
  install = function(m) ide:Print("Installed "..m) end,
  uninstall = function(m) ide:Print("Uninstalled "..m) end,
}

return {
  name = "...",
  description = "...",
  author = "...",
  version = 0.1,

  onRegister = function(self)
    -- add console alias with name "sample"
    ide:AddConsoleAlias("sample", commands)
  end,

  onUnRegister = function(self)
    -- remove console alias with name "sample"
    ide:RemoveConsoleAlias("sample")
  end,
}

When this plugin is activated, the user can then run sample.install() and sample.uninstall() commands in the local console.

Editor API

The editor object available through the plugin API is a wrapper around wxStyledTextCtrl object and supports all its methods. For example, editor:GetLineCount() will return the number of lines in the document. Note that all the numbers accepted by wxStyledTextCtrl API are zero-based, so to get the content of the first line in the documents, you should use editor:GetLine(0) instead of editor:GetLine(1).

Scintilla documentation may provide additional details where wxStyledTextCtrl documentation fails short. The names referenced by the Scintilla documentation are slightly different, but they should be easily guessable; for example, SCI_GETTEXT maps to editor:GetText()); although there are few exceptions: SCI_ASSIGNCMDKEY is mapped to CmdKeyAssign.

The menu object is a wrapper around wxMenu object and supports all its methods. For example, menu:AppendSeparator() will add a separator at the end of the current menu and menu:Append(id, "Popup Menu Item") will append a new item with id id to the menu.

Example: Basic plugin with all event handlers

A sample plugin is included with your IDE installation. It lists all existing event handlers and shows how to register various menu handlers. If you uncomment its code, it will show a message in the Output window for every event.

Example: Adding an event handler for typed characters

local pairs = {
  ['('] = ')', ['['] = ']', ['{'] = '}', ['"'] = '"', ["'"] = "'"}
local closing = [[)}]'"]]
return {
  name = "Auto-insertion of delimiters",
  description = [[Adds auto-insertion of delimiters (), {}, [], '', and "".]],
  author = "Paul Kulchenko",
  version = 0.1,

  onEditorCharAdded = function(self, editor, event)
    local keycode = event:GetKey()
    local char = string.char(keycode)
    local curpos = editor:GetCurrentPos()

    if closing:find(char, 1, true) and editor:GetCharAt(curpos) == keycode then
      -- if the entered text matches the closing one
      -- and the current symbol is the same, then "eat" the character
      editor:DeleteRange(curpos, 1)
    elseif pairs[char] then
      -- if the entered matches opening delimiter, then insert the pair
      editor:InsertText(-1, pairs[char])
    end
  end,
}

Example: Modifying main and popup menus

local id = ID("popupmenu.popupshow")
local iditem = ID("popupmenu.popupitem")
return {
  name = "Sample plugin with popup menu",
  description = "Sample plugin showing how to setup and use popup menu.",
  author = "Paul Kulchenko",
  version = 0.2,

  onRegister = function(self)
    -- add menu item that will activate popup menu
    local menu = ide:GetMenuBar():GetMenu(ide:GetMenuBar():FindMenu(TR("&Edit")))
    menu:Append(id, "Show Popup\tCtrl-Alt-T")
    ide:GetMainFrame():Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function()
      GetEditor():AddPendingEvent(wx.wxContextMenuEvent(wx.wxEVT_CONTEXT_MENU))
    end)
  end,

  onUnRegister = function(self)
    -- remove added menu item when plugin is unregistered
    ide:RemoveMenuItem(id)
  end,

  onMenuEditor = function(self, menu, editor, event)
    -- add a separator and a sample menu item to the popup menu
    menu:AppendSeparator()
    menu:Append(iditem, "Popup Menu Item")

    -- attach a function to the added menu item
    editor:Connect(iditem, wx.wxEVT_COMMAND_MENU_SELECTED,
      function(event) ide:Print("Selected popup item") end)
  end
}

Example: Adding a menu item and a toolbar button that run make

This plugin demonstrates how to add a menu item, add a toolbar button linked to that menu item, register an icon (in xpm format) for that toolbar button, and run an external command.

The plugin requires ZeroBrane Studio v0.71+ as it uses an API not available in earlier versions.

If you need to include a path to the filename in the current editor tab, something like the following may work:

ide:GetMainFrame():Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED,
  function ()
    local ed = ide:GetEditor()
    if not ed then return end -- all editor tabs are closed

    local file = ide:GetDocument(ed):GetFilePath()
    if file then -- a new (untitled) file may not have path
      ide:ExecuteCommand(('make "%s"'):format(file), ide:GetProject(), function(s) ide:Print(s) end)
    end
  end)

what a breath of fresh air. (doctorbling)

IMHO, ZBS is the fastest, most-flexible, best-thought-out IDE AND Debugger I've used since I began coding professionally in 1980. (Michael Berlyn)

Love ZeroBrane Studio. Lightweight. Works. Powerful. (Ross Berteig)

Love the debugger, saving my life. (Troy Sandal)

Thanks again for your awesome IDE. I've moved over to it, forsaking my previous investment in Decoda! (Stephen Nichols)

my kids love ZeroBrane. (Phil Hagelberg)

ZBS is beautiful experience. I am using it usually 10 hrs every day w/o problems. (Srdjan Markovic)

...heartily recommend it. (stun)

I love ZBS. I use it for all of the work I’ve started doing in Lua...! This IDE is fantastic! (Eddie Dover)

ZeroBrane Studio has got a working debugger, full IDE feature set, small footprint, and is completely customizable in lua. You have no idea how excited I was to find this little gem! (Jonathan Shieh)

It's a joy to use;... totally rock solid. (Jeff Smith)

Complaining about Zerobrane is like complaining that a beautiful spring day is a couple degrees too warm. (Jim Burnes)