Writing a simple Sublime Text plugin.

For the most part I like to keep my code editors as light and vanilla as possible. Some of the basic features that I like to see in my editor include auto indentation, syntax highlighting and ability to search across the project. Anything that will help debugging my codebase is a plus. Sublime Text offers all of these features out of the box and much more with the addition of community contributed plugins.

One of the neat Sublime Text features is that it provides you with a list of commands which you can extend (or write your own) and assign them to different key binds.

In this blog post I will go over configuring key binds and use insert_snippet command to generate some debug statements and then we will write few lines of python to extend insert_snippet to use text from clipboard as well.

Lets start with key mapping. If you go to Sublime Text preferences you will find a two item grouping for key binds (User and Default). Your new key binds should always go into User. Default should never be modified. If you wish to change any of the default key binds you can override them in the user config instead. Sublime Text will always load default config first, followed by OS specific config and user config last (each overriding previous definitions if needed).

Ok, lets add couple of simple key binds to our config file:

// Default.sublime-keymap
[
  { "keys": ["ctrl+shift+h"],
    "command": "insert_snippet",
    "args": {
      "contents": "console.log('=== HEARTBEAT $TM_FILENAME [$TM_LINE_NUMBER] ===');${0}"
    },
    "context": [{
      "key": "selector",
      "operator": "equal",
      "operand": "source.js",
      "match_all": true
    }]
  },
  { "keys": ["ctrl+shift+d"],
    "command": "insert_snippet",
    "args": {
      "contents": "console.log('=== $SELECTION $TM_FILENAME [$TM_LINE_NUMBER] ===', $SELECTION);${0}"
    },
    "context": [{
      "key": "selector",
      "operator": "equal",
      "operand": "source.js",
      "match_all": true
    }]
  }
]

The config is simple array of JSON objects, each containing a set of rules for particular key bind. I have configured two key binds: ctrl+shift+h and ctrl+shift+d. Both use insert_snippet command and are defined within the context of "source.js" (I will explain in a bit). We are passing "console.log(...)" as an argument to insert_snippet in both cases. This is the string that will be inserted at the cursor position once we use ctrl+shift+h or ctrl+shift+d. $TM_LINE_NUMBER, $TM_FILENAME and $SELECTION are environment variables which will be dynamically replaced by Sublime Text at insert time. The following snippet - ${0} will set the caret at this position once our console.log is generated.

The context allows you to write language specific key binds. In my case these will work with javascript files. You can have the same key bind with different implementations specific to the programming language you are working in. We can duplicate these two blocks and replace source.js with source.php to make it work with php and change console.log to print_r or dpm (or a different debug function) and Sublime Text will pick the correct snippet to insert depending on the language we are working in.

This is a sample output of the two key binds we defined above:

// ctrl+shift+h
// 477 is a line number
// some_file.js is the current js file we are working in.
console.log('=== HEARTBEAT some_file.js [477] ===');
 
// ctrl+shift+d
// 478 is a line number
// testvar was string we had selected when we pressed our key combination
// some_file.js is the current js file we are working in.
console.log('=== testvar some_file.js [478] ===', testvar);

So we have two key binds, one inserting a general debug heartbeat and the other one printing contents of a selected variable. That's ok so far, but I really wanted to be able to use ctrl+shift+d to create a var dump statement of a variable (string) that is in the clipboard as a fallback or use selected text as it behaves currently. Unfortunately insert_snippet doesn't have access to the clipboard content and we don't have environment variable that contains clipboard content either. There is a paste method in Sublime Text, but unfortunately we are unable to wrap arbitrary string around the clipboard content and can only paste clipboard content alone. We have exhausted all the available resources and will have to write few lines of python and create our own plugin/command that will extend the functionality of insert_snippet and allow it to use the contents from clipboard if needed.

Lets write our first Sublime Text plugin that will handle the functionality we outlined above. We start by going to Tools > New Plugin... which will generate a template for our new plugin. The code stub will look something like this:

import sublime, sublime_plugin
 
class ExampleCommand(sublime_plugin.TextCommand):
  def run(self, edit):
    self.view.insert(edit, 0, "Hello, World!")

I rewrote this template and my plugin looks something like this:

# insert_snippet_and_clipboard.py
import sublime, sublime_plugin
class InsertSnippetAndClipboardCommand(sublime_plugin.TextCommand):
  def run(self, edit, **args):
    for region in self.view.sel():
      if not region.empty():
        replacement = self.view.substr(region)
        args['contents'] = args['contents'].replace('$SELECTION_OR_CLIPBOARD', replacement)
        self.view.run_command('insert_snippet', args)
      else:
        replacement = sublime.get_clipboard().strip()
        args['contents'] = args['contents'].replace('$SELECTION_OR_CLIPBOARD', replacement)
        self.view.run_command('insert_snippet', args)

You can now save the file as insert_snippet_and_clipboard.py within packages/user/.
You can open a Sublime Text console using Ctrl+` and debug your new plugin during development by calling your command using view.run_command("example"). You can pass optional arguments to your command by passing them to run_command like this: view.run_command("example", args).

By following Sublime Text convention and naming our class SomeFunctionNameCommand(sublime_plugin.TextCommand): we are creating a text command named some_function_name. In our example we are creating insert_snippet_and_clipboard command which will provide user with $SELECTION_OR_CLIPBOARD environment variable. This environment variable will be populated at insert time. In this implementation we are prioritizing selected text, if no text is selected we are using the last clipboard snipped, and as a fallback we will replace the variable with an empty string.

And finally, lets update our key bind ctrl+shift+d to use insert_snippet_and_clipboard command:

// Default.sublime-keymap
[
  { "keys": ["ctrl+shift+h"],
    "command": "insert_snippet",
    "args": {
      "contents": "console.log('=== HEARTBEAT $TM_FILENAME [$TM_LINE_NUMBER] ===');${0}"
    },
    "context": [{
      "key": "selector",
      "operator": "equal",
      "operand": "source.js",
      "match_all": true
    }]
  },
  { "keys": ["ctrl+shift+d"],
    "command": "insert_snippet_and_clipboard",
    "args": {
      "contents": "console.log('=== $SELECTION_OR_CLIPBOARD $TM_FILENAME [$TM_LINE_NUMBER] ===', $SELECTION_OR_CLIPBOARD);${0}"
    },
    "context": [{
      "key": "selector",
      "operator": "equal",
      "operand": "source.js",
      "match_all": true
    }]
  }
]

And that is it. We should be able to generate some var debug statements right away by either selecting a piece of text (or copying it) and hitting ctrl+shift+d.

About the Author

Slavko Pesic, Developer

Slavko is a Web Programmer at Metal Toad Media and a Computer and Information Science graduate from University of Oregon. Introduced to Commodore 64 at the age of 5 he found his passion for technology and is eager to learn about anything tech related. Tinkering and solving puzzles is second nature to him.

In his spare time, Slavko can be found playing disc golf and ultimate frisbee as well as going camping/hiking/backpacking with friends. When in his natural setting (in front of a computer), he will spend hours reading articles on sciences and technology as well as playing video games.

Interested? Let's talk.