Labs/Bespin/DesignDocs/PluginAPI: Difference between revisions

From MozillaWiki
< Labs‎ | Bespin‎ | DesignDocs
Jump to navigation Jump to search
(New page: We want to allow people to extend the Bespin project in as many ways as possible. Right now there are a few ways to do so: * Create commands * Hack up your [[Labs...)
 
 
(10 intermediate revisions by 2 users not shown)
Line 8: Line 8:
For this we want a plugin API. The goal is for this API to be as simple as possible.
For this we want a plugin API. The goal is for this API to be as simple as possible.


As well as commands and the config work, we also have the low level construct of the publish/subscribe event system that you can tie into.
As well as commands and the config work, we also have the low level construct of the publish/subscribe event system that you can tie into. This shows that there are actually a couple of pieces to a plugin API:


Here is a short term go at a simple plugin system:
* First, we need an API that allows you to register and activate new plugin code
* Second, we need public APIs to let plugins easily get done what they need too, without having to go deep into the code, and accessing APIs that are a known public contract. This is vital to the goal of rapid innovation without constantly breaking plugins.


=== Wordpress-like ===
== Use Cases ==


What if we have a new directory, BespinSettings/plugins which is where your plugins go.
We want to drive the design of the plugin API from use cases. Can you think of any?


They can either be one file (myplugin.js) or a directory (myplugin/plugin.js with other stuff too).
==== Commands ====


To activate a plugin, you add a line to your config file (loadplugin pluginname say). We don't automatically load all plugins JUST in case there is an issue, or you want to easily activate and deactivate plugins without having to delete files. In theory we could create a "loadplugin all" semantic for those that really want it.
A set of commands, such as a new command store with subcommands (e.g. vcs)


To get a plugin into the system, we need to piggy back on the ability to import code into a project. Right now we have an explicit import command that ends up with a new project. We need to also allow you to import a piece into an existing project. In this case, into the "plugin" directory under the magic "BespinSettings" directory. This is a dependency. We also want to let you do this not just by grabbing a URL, but also via file upload of a .zip|tgz|...
==== Syntax Highlighter ====


The plugin itself should have a set of metadata associated with it. This can be placed into it via comments, a la Wordpress.
Attach a new syntax highlighter


We will have new events for items such as:
==== Code Processing ====


* bespin:plugin:activate
Such as:
* bespin:plugin:deactivate
* bespin:plugin:load:complete


The plugin will listen in on these and will then hook itself up.
* real-time syntax checking
* special processing for certain types of files
* spell checker type work on code


How does a plugin change the default behaviour? What if we change bespin.publish/subscribe to have an id, or a type associated with the event. All default behaviour will pass in a "default" or whatever makes sense.
When you open up a settings file, limit what you can type so it is only "name value" and allow the user to rerun that file


Then, the plugin, can unsubscribe() on the type default when it adds itself to the subscription queue.
==== Editor Tweaks ====


We also need to think about how data, or images, or JSON can be used from within a plugin (using its own resources). Furthermore, having a simple include() from within the config and plugin work would be nice. This could piggy back on dojo.require/provide.
Add new features to the core editor itself. Mucking with the model.


==== Discovery ====
==== Toolbar ====  


Another part of plugins, is having people find them! A natural fit would be to use the Mozilla Add-ons infrastructure (AMO) to host plugins.
Add new buttons and icons to the toolbar


==== Cross domain loading ====
==== Selection ====


With a cross domain Dojo build in place, we will be able to load plugins that live remotely.
When text is selected, hook in


In your config file you will be able to fire an event to load a plugin:
==== Content Changed ====


bespin.publish("bespin:plugin:load", {
As soon as the content in the edit buffer has changed, do something (meta with implementations below)
   url: "http://foo.com/bespinPlugin.zip"
 
});
==== Settings ====
 
Create new settings
 
== Other useful features ==
 
* Be able to use a Bespin project as a plugin (while working on plugins). Possibly even be able to use a directory within a Bespin project.
* Be able to reload plugins that you're working on
* Plugins should be loaded lazily to improve startup time. For example, a plugin that provides a syntax highlighter shouldn't be loaded until that highlighter is needed.
 
== Plugin Definition and Loading ==
 
Here we will talk about the first goal, the ability for plugins to load and be activated.
 
=== A one-file plugin ===
 
Let's start with an example:
 
    // Typically, we'll start with some metadata. This metadata must be valid JSON
    // (no functions in the metadata).
    exports.info = {
        // Plugins all have a name that represents the namespace of the plugin
        // This allows the plugin system to locate any functions that are
        // referred to by name
        name: "luaHighlighter",
       
        // up to the first "." would be used as the short description. Everything
        // else is viewed in a long description context.
        description: "Syntax highlighter for the Lua programming language.",
       
        // version numbers will be good for automatic updates.
        version: "1.0",
       
        // core parts of Bespin (and even plugins) can query for metadata
        // and request that a plugin is loaded. In this case,
        // Bespin's simple syntax highlighting engine will look for plugins
        // when it opens a file type it doesn't know how to handle.
        // If it's a lua file, it will see that this plugin can handle
        // lua.
        provides: [
            ["bespin.syntax.simple", {
                extensions: ["lua"],
                // in the single file version of a plugin, you just refer to the
                // functions in the plugin itself
                load: "luaHighlighter"
            ]
        ],
        subscribes: [
            ["file:savefile", "savehandler"]
        ]
    }
   
    exports.luaHighlighter = function() {
        // return a lua highlighter instance
    }
   
    exports.savehandler = function(event) {
        // do whatever it is we do on save
    }
   
 
Plugin modules are implemented not as Dojo modules, but rather as [https://wiki.mozilla.org/ServerJS/Modules/SecurableModules ServerJS Securable Modules]. This would allow seamless interop for plugins that have both client and server side JS components.
 
 
Here's an example of a single file plugin that adds commands and adds/removes a DOM element:
 
    exports.info = {
        name: "Something Fun",
        provides: [
            ["bespin.command", {
                name: "woot",
                pointer: ":woot"
            }],
            ["bespin.command", {
                name: "hello",
                takes: ["name"],
                pointer: ":hello"
            }]
        ]
    }
   
    exports.woot = function(instruction) {
        instruction.addOutput("woot!");
    }
   
    exports.activate = function() {
        dojo.create("div", {
            id: "fun",
            style: "opacity: 0; position: absolute; top: 0; left: 0; padding: 2em; background: white"
        }, document.body);
    }
   
    exports.deactivate = function() {
        dojo.query("#fun").orphan();
    }
   
    exports.hello = function(instruction, name) {
        instruction.addOutput("Hi there, " + name);
    }
 
=== Plugin installation ===
 
New commands will allow you to work with plugins:
 
;plugin install URL|Bespin file path:Install a plugin. You can point it at a single file plugin, or at a zip file or tarball. If it's a zip or tgz, it needs to have a single .js file in it or contain a directory that includes a plugin.json. plugin.json provides the equivalent of the info object above. You can also give it a Bespin file path pointing to a directory or file where there is a plugin.
;plugin uninstall PluginName:If installed from a remote location, the plugin files will be deleted. If it was a Bespin path, the plugin is just removed from the installed plugin data.
;plugin reload:generally used in development of a plugin.
;plugin list:list the installed plugins.
 
Bespin keeps track of installed plugins in a file called BespinSettings/plugins.metadata. The plugins themselves are installed into BespinSettings/plugins/. The metadata file includes all of the info objects from all of the plugins. Additionally, it stores where the plugin came form and where it is located within Bespin.
 
When a plugin is installed, the module is loaded and the info object is extracted. For this reason, plugin modules should not actually *do* anything when they are loaded. The call to activate() is the time when the plugin should actually do something.
 
=== Plugin loading ===
 
The idea behind this plugin system is to load as little as possible. Bespin will always load the plugins.metadata file. Based on the metadata, Bespin will decide when to load a plugin. The exact timing of loading the code will depend on what the plugin does. For example, syntax highlighters are loaded when a file of the appropriate type is opened.
 
It will be possible to have a plugin load immediately, but the goal is to seek out ways to allow plugins to load more lazily.
 
=== Singletons ===
 
Plugins are singletons; there is only one instance of a plugin in memory at a time.
 
=== Disabling Plugins ===
 
Adding ?disable=ALL to the editor URL will turn off all plugins. Adding ?disable=PluginName will disable a single plugin. This can be used as an escape hatch if a plugin is ill-behaved and renders Bespin inoperable.
 
== Plugin API ==
 
Functions available within the plugin's loading scope:
 
    // require() loads a module. Modules are loaded only once, so calling
    // require a second time will simply return the same object.
    // var module = require("./modulename");   Example:
    var narcissus = require("narcissus");
    var ast = narcissus.parse("1+1");
   
    // resourceURL() computes URLs to resources relative to code modules.
    // If only one parameter is provided, the URL is relative to the plugin.json file.
    // Example:
    var url = resourceURL("bespin", "../../images/foo.png");
   
    var url = resourceURL("images/foo.png"); // images directory at same level as plugin.json
   
    // subscribe() is like bespin.subscribe, except the plugin handler automatically
    // keeps track of subscriptions and will unsubscribe if the plugin needs to
    // be reloaded or deactivated.
    subscribe("plugin:activated", function(plugin) {
        // do something
    });
   
 
=== Bespin Extension API ===
 
The sections above describe the basic framework for handling plugins. The API for extending Bespin will be developed over time, based entirely upon the development of real-world plugins. Documentation in this spot will grow along with that API.

Latest revision as of 16:18, 14 August 2009

We want to allow people to extend the Bespin project in as many ways as possible. Right now there are a few ways to do so:

This is fine for little things, but what about sharing functionality?

For this we want a plugin API. The goal is for this API to be as simple as possible.

As well as commands and the config work, we also have the low level construct of the publish/subscribe event system that you can tie into. This shows that there are actually a couple of pieces to a plugin API:

  • First, we need an API that allows you to register and activate new plugin code
  • Second, we need public APIs to let plugins easily get done what they need too, without having to go deep into the code, and accessing APIs that are a known public contract. This is vital to the goal of rapid innovation without constantly breaking plugins.

Use Cases

We want to drive the design of the plugin API from use cases. Can you think of any?

Commands

A set of commands, such as a new command store with subcommands (e.g. vcs)

Syntax Highlighter

Attach a new syntax highlighter

Code Processing

Such as:

  • real-time syntax checking
  • special processing for certain types of files
  • spell checker type work on code

When you open up a settings file, limit what you can type so it is only "name value" and allow the user to rerun that file

Editor Tweaks

Add new features to the core editor itself. Mucking with the model.

Toolbar

Add new buttons and icons to the toolbar

Selection

When text is selected, hook in

Content Changed

As soon as the content in the edit buffer has changed, do something (meta with implementations below)

Settings

Create new settings

Other useful features

  • Be able to use a Bespin project as a plugin (while working on plugins). Possibly even be able to use a directory within a Bespin project.
  • Be able to reload plugins that you're working on
  • Plugins should be loaded lazily to improve startup time. For example, a plugin that provides a syntax highlighter shouldn't be loaded until that highlighter is needed.

Plugin Definition and Loading

Here we will talk about the first goal, the ability for plugins to load and be activated.

A one-file plugin

Let's start with an example:

   // Typically, we'll start with some metadata. This metadata must be valid JSON
   // (no functions in the metadata).
   exports.info = {
       // Plugins all have a name that represents the namespace of the plugin
       // This allows the plugin system to locate any functions that are
       // referred to by name
       name: "luaHighlighter",
       
       // up to the first "." would be used as the short description. Everything
       // else is viewed in a long description context.
       description: "Syntax highlighter for the Lua programming language.",
       
       // version numbers will be good for automatic updates.
       version: "1.0",
       
       // core parts of Bespin (and even plugins) can query for metadata
       // and request that a plugin is loaded. In this case,
       // Bespin's simple syntax highlighting engine will look for plugins
       // when it opens a file type it doesn't know how to handle.
       // If it's a lua file, it will see that this plugin can handle
       // lua.
       provides: [
           ["bespin.syntax.simple", {
               extensions: ["lua"],
               // in the single file version of a plugin, you just refer to the
               // functions in the plugin itself
               load: "luaHighlighter"
           ]
       ],
       subscribes: [
           ["file:savefile", "savehandler"]
       ]
   }
   
   exports.luaHighlighter = function() {
       // return a lua highlighter instance
   }
   
   exports.savehandler = function(event) {
       // do whatever it is we do on save
   }
   

Plugin modules are implemented not as Dojo modules, but rather as ServerJS Securable Modules. This would allow seamless interop for plugins that have both client and server side JS components.


Here's an example of a single file plugin that adds commands and adds/removes a DOM element:

   exports.info = {
       name: "Something Fun",
       provides: [
           ["bespin.command", {
               name: "woot",
               pointer: ":woot"
           }],
           ["bespin.command", {
               name: "hello",
               takes: ["name"],
               pointer: ":hello"
           }]
       ]
   } 
   
   exports.woot = function(instruction) {
       instruction.addOutput("woot!");
   }
   
   exports.activate = function() {
       dojo.create("div", {
           id: "fun",
           style: "opacity: 0; position: absolute; top: 0; left: 0; padding: 2em; background: white"
       }, document.body);
   }
   
   exports.deactivate = function() {
       dojo.query("#fun").orphan();
   }
   
   exports.hello = function(instruction, name) {
       instruction.addOutput("Hi there, " + name);
   }

Plugin installation

New commands will allow you to work with plugins:

plugin install URL|Bespin file path
Install a plugin. You can point it at a single file plugin, or at a zip file or tarball. If it's a zip or tgz, it needs to have a single .js file in it or contain a directory that includes a plugin.json. plugin.json provides the equivalent of the info object above. You can also give it a Bespin file path pointing to a directory or file where there is a plugin.
plugin uninstall PluginName
If installed from a remote location, the plugin files will be deleted. If it was a Bespin path, the plugin is just removed from the installed plugin data.
plugin reload
generally used in development of a plugin.
plugin list
list the installed plugins.

Bespin keeps track of installed plugins in a file called BespinSettings/plugins.metadata. The plugins themselves are installed into BespinSettings/plugins/. The metadata file includes all of the info objects from all of the plugins. Additionally, it stores where the plugin came form and where it is located within Bespin.

When a plugin is installed, the module is loaded and the info object is extracted. For this reason, plugin modules should not actually *do* anything when they are loaded. The call to activate() is the time when the plugin should actually do something.

Plugin loading

The idea behind this plugin system is to load as little as possible. Bespin will always load the plugins.metadata file. Based on the metadata, Bespin will decide when to load a plugin. The exact timing of loading the code will depend on what the plugin does. For example, syntax highlighters are loaded when a file of the appropriate type is opened.

It will be possible to have a plugin load immediately, but the goal is to seek out ways to allow plugins to load more lazily.

Singletons

Plugins are singletons; there is only one instance of a plugin in memory at a time.

Disabling Plugins

Adding ?disable=ALL to the editor URL will turn off all plugins. Adding ?disable=PluginName will disable a single plugin. This can be used as an escape hatch if a plugin is ill-behaved and renders Bespin inoperable.

Plugin API

Functions available within the plugin's loading scope:

   // require() loads a module. Modules are loaded only once, so calling
   // require a second time will simply return the same object.
   // var module = require("./modulename");   Example:
   var narcissus = require("narcissus");
   var ast = narcissus.parse("1+1");
   
   // resourceURL() computes URLs to resources relative to code modules.
   // If only one parameter is provided, the URL is relative to the plugin.json file.
   // Example:
   var url = resourceURL("bespin", "../../images/foo.png");
   
   var url = resourceURL("images/foo.png"); // images directory at same level as plugin.json
   
   // subscribe() is like bespin.subscribe, except the plugin handler automatically
   // keeps track of subscriptions and will unsubscribe if the plugin needs to
   // be reloaded or deactivated.
   subscribe("plugin:activated", function(plugin) {
       // do something
   });
   

Bespin Extension API

The sections above describe the basic framework for handling plugins. The API for extending Bespin will be developed over time, based entirely upon the development of real-world plugins. Documentation in this spot will grow along with that API.