Monday, April 2, 2012

Creating Plug-ins for Sublime Text 2


I have been trying out Sublime Text 2 as my text editor lately, and I'm loving the simplicity, so I figured I would try out creating a plug-in for it. I was pleasantly surprised at how easy it is, which is an important step towards it becoming my new editor of choice. I wanted to take some steps towards creating something along the lines of rinari, but for Scala... in Sublime Text. I was able to fairly easily easily create a plug-in that allowed me to run the Scala Test that was currently open in the editor, or run all Scala Tests in the (inferred) project folder, or switch back and forth between a test and the code under test, or quickly navigate to any scala file in the project folder with a few keystrokes. This post will show you how to create a new plug-in for Sublime Text 2, which uses all the API features that I needed to implement that functionality.

Create a new plug-in

Step 1. Install Sublime Text 2 (see link above). Its free to try, and fairly cheap to buy. A month or so after you download it, it basically becomes nag-ware until you finally manage to overcome your stingy developer impulses and plunk down the $59 to buy it. Also, unlike other similar text editors (ahem.. TextMate!) it actually runs on Windows and Linux, as well as Mac OSX.
Step 2. Create a new folder for your plug-in. On Mac OSX, this goes under your home folder in ~/Library/Application Support/Sublime Text 2/Packages/{PLUGIN_NAME} (where in my case, {PLUGIN_NAME} was "ScalaTest").
Step 3. Create a python file which will contain the code for the plug-in. (Name it whatever you want, as long as it ends in ".py" ;-) Here is a really basic plug-in (borrowed from this plug-in tutorial, which you should read after this):
import sublime, sublime_plugin

class ExampleCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    self.view.insert(edit, 0, "Hello, World!")
Right, so, as I mentioned: Sublime Text 2 plug-ins are written in Python. Don't worry too much if you're not familiar with Python... I wasn't either prior to starting this experiment, and it didn't prove to be too much a problem. (I did have a couple Python books laying around, but I'm sure the same information is on the tubes.) Its fairly easy to pick up, and has some similarities to Ruby, in case that helps. So the code above creates a command called "example" which is defined by a class that inherits from Sublime Text's "TextCommand" class. (Sublime Text 2 maps the title-case class names to underscore-delimited command names, and strips the "Command" suffix.) All the plug-in does is insert the text "Hello, World!" at the beginning of the file open in the editor.
(Note: Sublime Text 2 will detect that you created a Python file under its plug-in folder and automatically loads it.)
Step 4. Run your example. Hit Ctrl+Backtick to open the python interpreter within Sublime Text 2. Run your command by typing in this:
view.run_command("example")
The open buffer will now include the aforementioned greeting. You could bind it to a key-combination easily enough, but hey, it doesn't do anything cool yet, right?, so we'll hold off on the key bindings until the end.

Make it do something cool

So that you can see these approaches in action, I uploaded my nascent ScalaTest plug-in to github:https://github.com/patgannon/sublimetext-scalatest. Note that this plug-in will currently only work with projects that use Bizo's standard folder structure, and has a hard coded path to the scala executable, so its not ready to be used as-is. I hope to clean it up in the future and make it more generically applicable, but for now, I've only shared it to add a bit more color to the code snippets in this section.

Run a command on the current file

The name of the file currently open in the editor can be obtained with this expression: self.view.file_name(). In my plug-in, I use that to infer a class name, the project root folder, and path to the associated test (using simple string operations).
You can create an output panel (in which to render the results of running a command on the open file) by calling: self.window.run_command("show_panel", {"panel": "output.tests"}) (where "output.tests" is specific to your plug-in). In my plug-in, I created the helper methods below to show the panel and clear out its contents. (See the BaseScalaTestCommand class in run_scala_test.py). Note that this code was derived from code I found in theSublime Text 2 Ruby Tests plug-in.
 def show_tests_panel(self):

  if not hasattr(self, 'output_view'):

   self.output_view = self.window().get_output_panel("tests")

  self.clear_test_view()


  self.window().run_command("show_panel", {"panel": "output.tests"})



 def clear_test_view(self):

  self.output_view.set_read_only(False)

  edit = self.output_view.begin_edit()


  self.output_view.erase(edit, sublime.Region(0, self.output_view.size()))

  self.output_view.end_edit(edit)

  self.output_view.set_read_only(True)
(Note: I don't recommend copy/pasting code directly from this blog post, because the examples are pasted in from github, which messes up the indentation, which is a real problem in Python; instead, clone the github repository and copy/paste from the real file on your machine.)
To actually execute the command, I use this code in my run method, after calling show_tests_panel defined above (note that you will need to import 'subprocess' and 'thread' at the top of your plug-in file):
  self.proc = subprocess.Popen("{my command}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

  thread.start_new_thread(self.read_stdout, ())


...where {my command} is the shell command I want to execute, and read_stdout is a method I defined which copies the output from the process and puts it into the output panel. Its defined as follows (and calls the append_data method, also defined below):
 def read_stdout(self):

  while True:

   data = os.read(self.proc.stdout.fileno(), 2**15)



   if data != "":

    sublime.set_timeout(functools.partial(self.append_data, self.proc, data), 0)


   else:

    self.proc.stdout.close()

    break
 def append_data(self, proc, data):

  self.output_view.set_read_only(False)

  edit = self.output_view.begin_edit()

  self.output_view.insert(edit, self.output_view.size(), data)


  self.output_view.end_edit(edit)

  self.output_view.set_read_only(True)
(Note: Depending on the command you're running, you may also want to capture the process' stderr output, and also put that into the output panel, using a variation of the approach above.)

Using the "quick panel" to search for files, and opening files

The "quick panel" (the drop-down which lists files when you hit command-T in sublime-text) can be extended to have plug-in specific functionality, which I used to create a hot-key for quickly navigating to any Scala file under my project folder. (See the JumpToScalaFile class in run_scala_test.py.) One of the plug-in examples I saw using the quick panel sub-classed sublime_plugin.WindowCommand instead of TextCommand. This results in a plug-in which can be run without any files being open. The flip side of that, though, is you don't get the file name of the currently open file, which in my case, is required to infer the base project folder for which to search for files. Thus, all my plug-ins sub-class TextCommand. To open the quick panel, execute: sublime.active_window().show_quick_panel(file_names, self.file_selected). file_names should be a collection of the (string) entries to show in the quick panel. Note that the entries don't have to be file paths, just a convenient identifier to show the user (in my case, the class name). file_selected is a method you will define which will be called when a user selects an entry in the quick panel. Here's how I defined it:
 def file_selected(self, selected_index):

  if selected_index != -1:

   sublime.active_window().open_file(self.files[selected_index])


self.files is an array I created when populating the quick panel which maps an index in the quick panel to a file path. I then use sublime.active_window().open_file to open that file in Sublime Text.
I also used that same method (open_file) in the plug-in that automatically navigates back and forth between a test file and the code under test. That plug-in also makes use of the sublime.error_message method, which will display an error message to the user (if no test is found, for example).

Create keystroke bindings

To bind your new plug-in commands to keystrokes, create a file in your plug-in folder called Default (OSX).sublime-keymap. This will contain the keystrokes that will be used on Mac OSX. (You would create separate files for use on Windows and Linux.) It is a simple JSON file that maps keystrokes to commands. Lets see an example:
[

 { "keys": ["super+shift+e"], "command": "jump_to_scala_file" }

]
This example will bind Command+Shift+e to the "jump_to_scala_file" command (defined in the JumpToScalaFileCommand class in any plug-in). If you have multiple key-mappings, you would create multiple comma-delimited entries within the JSON array. (See the example in my plug-in.) In order to reduce the possibility of defining keystrokes that collide with keystrokes from other plug-ins, I defined mine in such a way that they're only available when the currently open file is a Scala file. Here is the rather verbose (ahem, powerful) syntax that I used to do that:
[

 { "keys": ["super+shift+e"], "command": "jump_to_scala_file", 

  "context" : [{"key": "selector", "operator": "equal", "operand": "source.scala", "match_all": true}]}


]

Conclusion

Over the years, I've grown to prefer light-weight editors (such as emacs or Sublime Text 2) over more heavy-weight IDEs (such as Eclipse or Visual Studio) because they don't tend to lock up in the middle of writing code and/or crash sporadically, and I generally don't need a lot of whiz-bang features when I'm coding these days. I used emacs (and rinari) for doing rails development for a year or so, but the basic key-strokes (compared to the de-facto text editing standard key-strokes) and the undo/redo functionality always seemed a bit awkward, especially when you wind up switching back and forth between that and other text editors. Also, the language for creating plug-ins is Scheme (a dialect of Lisp), which to me isn't very convenient for these sort of things.
I was really pleased with my foray into creating plug-ins for Sublime Text 2, and combined with its general ease of use, I've decided its now my new favorite editor. Using an editor that's this easy to significantly customize seems like it could be a real productivity win over time. Given the fairly rich list of plug-ins already available, I think the future is bright for Sublime Text 2. Below are a list of resources I found helpful during this process, including said list of plug-ins.

Resources

Unofficial list of plug-ins:
http://wbond.net/sublime_packages/community

No comments: