I want to write plugins for Atom's editor in Ruby. Opal makes this possible. Atom is one of several projects in recent times to combine Chromium with Node.js for a desktop app. While it utilizes chromium for it's gui, and boasts "[e]very Atom window is essentially a locally-rendered web page", writing Atom plugins is more like writing a server-side node.js app than a typical single-page client-side app (albeit with really awesome integration with Chrome Devtools). Opal development, on the other hand, has to-date been focused primarily on the browser use-case.
Because of this, I had to make a choice between using the opal-node package from npm, using Opal via Ruby w/ a compile step, or packaging up opal-parser.js, including it with the app, and writing in compilation on the fly. Each choice came with compromises. Using opal-node would have been easiest, just create a top level index.coffee that required opal-node, and then require in your ruby scripts beneath that. But opal-node typically lags behind the main opal repo, and it would make integration with the rest of the ruby ecosystem a little harder. Including opal-parser.js could have been made to work, but I didn't see any real benefits to going that route. So I set up a rake task, at the suggestion of @ryanstout, to compile my ruby into javascript.
Unfortunately, something about node, I believe it's at least related to the module system, means code generated by the Opal compiler for the browser, will not simply just run. As an example, this ruby code:
puts "Hello, Opal and Node"
Compiles into this js (along with several thousand LOC that are the opal corelib and runtime):
/* Generated by Opal 0.6.0 */
(function($opal) {
var self = $opal.top, $scope = $opal, nil = $opal.nil, $breaker = $opal.breaker, $slice = $opal.slice;
$opal.add_stubs(['$puts']);
;
return self.$puts("Hello, Opal and Node");
})(Opal);
//# sourceMappingURL=/__opal_source_maps__/hello.js.map
;
But sadly, if we try running this via node we see errors.
$ node lib/hello.js
/Users/ericwest/code/projects/samples/opal_node_test/lib/hello.js:1028
})(Opal);
^
ReferenceError: Opal is not defined
at Object.<anonymous> (/Users/ericwest/code/projects/samples/opal_node_test/lib/hello.js:1028:4)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
at startup (node.js:119:16)
at node.js:901:3
So, I to make a long story short, I borrowed a bit of code from opal-node that would let me get the best of both worlds. The code I am using is based on Opal-master, which is a soon-to-be-released '0.6'. Versions of Opal older than this will require 'opal-sprockets', I believe. Check out the documentation.
My setup is:
plugin-folder/ | --lib/ | --js/ | --opal.js --plugin.js | --plugin.coffee | --src/ | --plugin.rb | Rakefile package.json
In the package.json, set your entry point (main) to lib/plugin
, which refers to plugin.coffee. This will give us a place to create the setup we need to get things working together.
{
"name": "plugin",
"main": "./lib/plugin",
"version": "0.0.0",
"private": true,
"description": "A short description of your package",
"activationEvents": ["plugin:say_hi"],
"repository": "https://github.com/atom/plugin",
"license": "MIT",
"engines": {
"atom": ">0.50.0"
},
"dependencies": {
}
}
Next, create your Rakefile. This will compile your ruby to javascript. It wouldn't be too hard to set-up automatic compilation using Guard. Just need to actually do it.
# Rakefile
require 'opal'
desc "Build the Opal runtime and corelib"
task :opaljs do
opal = Opal::Builder.build('opal')
File.open("lib/js/opal.js", "w+") do |out|
out << opal.to_s
end
end
desc "Build our app to ./lib/js/plugin.js"
task :build => :opaljs do
env = Opal::Environment.new
env.append_path "src"
File.open("lib/js/plugin.js", "w+") do |out|
out << env["plugin"].to_s
end
end
Running rake build
, when we are ready, will compile our ruby code and also create the opal.js
runtime and corelib. We'll require in anything we need from the opal stdlib in our plugin.rb file.
plugin.coffee
solves the earlier error message for us, and lets us interact easily with the Atom api as well as any node.js modules we might want to use.
sourceFile = "#{__dirname}/js/opal.js"
fs = require('fs')
source = fs.readFileSync(sourceFile).toString()
vm = require 'vm'
vm.runInThisContext(source)
require('./js/plugin.js')
module.exports =
activate: ->
atom.workspaceView.command "plugin:say_hi", => Opal.Plugin.$new().$say_hi()
You can read about what the Atom specific code does in their docs. There may be an easier/simpler/superior way to get this to work, but once I found a way which did, I moved on.
Finally, our ruby code src/plugin.rb
require 'native'
class Plugin
include Native
attr_accessor :atom, :editor
def initialize
@atom = `atom`
@editor = `atom.workspaceView.getActivePaneItem()`
end
def say_hi
editor = Native(@editor)
editor.insertText('Hello, World!')
end
end
Notice we do not require "opal"
in our plugin.rb script. We've already loaded that, and if we require it here it will get concatenated onto the script and end up hindering the performance of our plugin. For me, getting the atom API calls wrapped just right was a little tricky, but that's mostly because Opal has changed substantially from the last time I used it seriously (almost a year ago). It would be really cool if someone wrote a nice opal library wrapping the Atom API. I, personally, will consider it once Atom's development becomes a little less volatile. Anyways, if you run your command in a buffer it will print Hello, World!
(Be careful and don't test it in one of your plugin's scripts).
I'll be working on a nice surprise for Atom-using rubyists as I can get time, involving the work I did previously on Rsense. What will you build?
Thats great news! Would really love to be able to develop Atom plugins using Ruby!!!