Sublime Text 2 tips for Python and web developers

Update 2014-03: Please see updated blog post for Sublime Text 3.

Sublime Text 2 is a very powerful text editor which has gained popularity recently – for good reasons. It is commercial (59 USD). Plenty of power comes from the fact that Sublime has a plug-in framework built in Python. Even though the core app remains closed there exist a vibrant plug-in ecosystem around the editor.

Note: You can try Sublime for free. It simply gives nagging dialog “Please buy” now and then.

Here is my collection of tips how I squeezed more power out of the editor. The tips are written from OSX perspective, but should work fine on Linux and Windows too.

I used to be Aptana Studio (Eclipse) junkie. Even though Sublime does not have all the GUI power of Aptana (e.g. see the end of post here) I have found the current versions of Sublime serving my needs better. The biggest reason for the switch is that Aptana / Eclipse forces folders and files to be encapsulated inside “Eclipse space” to work well.

Sublime is more suitable for work where you work with various open source components which do not follow such rigid order you might find in in-house corporate software. When you need to integrate different tools and projects, Sublime scripting opportunities are more straightforward versus building thousands of lines Java code what could be need with Eclipse.

Note: Don’t write anything about Vim or Eclipse in this paragraph.

1. Unofficial manual

There exist a third-party maintained manual for Sublime. Especially parts for settings and keyboard shortcuts come very handy.

2. Add-on manager

Install Sublime Package Control. You need run this in order to install any Sublime Text plug-ins. It magically pulls plug-ins from thin air (or GitHub).

After Package Control has been installed you can add new packages with CMD + SHIFT + P, search Package Install. It has context sensitive search and you can find all packages by name.

3. Favorite plug-ins list

Based on the feedback from the blog friends you might want to install the following plug-ins through Sublime Package Control

More about specific plug-ins later.

4. Open files from command-line

I have the following in my shell .rc file, so I can open files directly from the terminal:

alias subl="'/Applications/Sublime Text'"
alias nano="subl"
export EDITOR="subl"

Note: nice fallback for nano command which comes from the muscle memory sometimes.

5. Open folders as projects from command-line

You can also open folders in Sublime Text.

Just type e.g.

subl src

… and the whole src/ folder is opened in the Sublime Text project explorer (right hand).

Note: One folder = one project = one window? I am not sure if there are ways to have multiple projects in the same window.

6. Searching multiple files

First open a folder as a project in Sublime Text 2. You can do this from the command line, as instructed above, or from File > Open menu.

Then right click the folder in the sidebar to search it:

You can also specify a file extension mask as a comma separated in the Where: field.

7. Configure sane tab and whitespace policy and other settings

Never save your files with hard tabs characters in them. The same goes for trailing whitespaces which are against policy of many programming language style guides.

In the menu Sublime Text 2 > Preferences > File Settings – User drop in this snippet:

// Place user-specific overrides in this file, to ensure they're preserved
// when upgrading. See
// for more info

    // Tab and whitespace handling.
    // Indent using spaces, 4 spaces ber indent by default, clean up extra whitespaces on save
    "tab_size": 4,
    "translate_tabs_to_spaces": true,
    "trim_automatic_white_space": true,
    "trim_trailing_white_space_on_save": true,

    // Do not try to detect the tab size from the opened file
    "detect_indentation" : false,

     // Don't do any VIM keybindings or other VIM fanatic sugar
    "ignored_packages": ["Vintage"],

    // Don't complain about plug-in responsive
    // (Too verbose when you have slow computer)
    "detect_slow_plugins": false,

    // The delay, in ms, before the auto complete window is shown after typing
    "auto_complete_delay": 500,

    // Install better them
    // (You can get this from the package control)
    "theme": "Soda Dark.sublime-theme",

    // Download better font

    "font_face" : "Source Code Pro",
     // Don't show hidden (dotted) directories and binary files in the sidebar
    // (You are not going to edit them in any case)
    "file_exclude_patterns": [".*", "*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db"]

See more about the settings.

Note: Don’t confuse File Settings – User with Global Settings – User in Preferences menu. The latter doesn’t seem to work.

Note: Even though you configure this policy, Sublime Text 2 may auto-detect another tab_size policy when you open a file. See the explanation below.

8. Converting existing files to use spaces instead of tabs

Sublime tries to autodetect tab settings settings for every opened file and may fail, so keep your eye on this when working with uncleaned files. You might want to use git pre-commit to prevent committing files with tabs in them.

Do View > Indentation > Convert Indentation to Spaces and make sure Indent using spaces is turned on in the same menu. The new versions of Sublime should remember this setting on file type basis.

There are also config files which you can access from Preferences menu, but after many failed attempts and hacking several config files it did me no good. Maybe autodetect was overriding my simply attempt of never use tab.

9. Map file formats to syntax highlighting

If you a have a file format you want to recognize under a certain highlighter e.g. map ZCML files to XML highlighter.

Open any file of the format.

Then: View > Syntax > Open all with current extension as… ->[your syntax choice].

ZCML, now with full color

More info.

10. Disable automatic loading of the last session

By default, Sublime Text re-opens all files and folders you had when you closed the editor last time. Some people don’t like this.

Instructions to disable automatic session restore for Sublime Text.

11. Lint and validate your files while typing

SublimeLinter scans your files on the background when typing using validators and linters for various errors. Please see Configuration section in README as you might need to install additional software, like Node.js, to run some of the linters.

Detect mistypes CSS, yay!

SublimeLinter comes with built-in pylint and has Node’s jshint and csslint packages includes

12. Add CodeIntel autocompletion support

Install CodeIntel from Package Control.

If you are working with Python projects, using buildout, this recipe comes to handy.

recipe =
eggs = ${instance:eggs}
extra-paths =

This will generate .codeintel file inside your buildout folder.

CodeIntel plug-in assumes .codeintel file is in your project root. Simply type

subl .

to open your buildout folder as Sublime project folder.

Now Sublime should support auto-completion. E.g. start typing

from zope.interface import <--- Auto completion pops up here

Also, with CodeIntel, ALT + mouse click takes you the source code of import, or the declaration of any function you click.

CodeIntel also supports PHP, Ruby and Javascript, to name few.

Note: If you are not using buildout, or Python, you can always create CodeIntel configuration file in old-fashioned way.

13. Go to anywhere shortcut

CMD + P. Type in a part of a filename and a part of a function / rule name. You are there. Very powerful, yet so simple feature.

14. Go to line shortcut

Use Go To Line functionality CTRL+G for more traditional jumps.

15. Context sensitive in-file search shortcut

Handy for Javascript, CSS, Python, etc. CMD + R. Type your method or rule name and Sublime automatically jumps into its declaration.

… or in Python …

16. Edit multiple words or lines simultaneously using multi cursor

This trick is handy if you need to wrap / unwrap stuff in quotes, add commas, add parenthesis etc. on multiple lines or items simulatenously.

First select lines or items. You can select multiple individual words by holding down CMD and double clicking words. For lines you can do just the normal SHIFT selection.

Press SHIFT + CMD + L to activate the multi cursor mode.

Then edit all the entries simultaneously. Use CMD + left and CMD + right etc. to move al the cursors to the beginning or the end of the linen and so on.

17. Open OS file browser for the currently opened file or any of its parent directories

CTRL + mouse click filename in the title bar of the edit window to show the full path to the file and open any of its parent folder.

Note: This is OSX’s Finder file browser standard behavior and might not work on other platforms.

18. More powerful sidebar

Install SideBarEnhanchements package to get more menu entries to sidebar which you can use to manage your source folder.

19. Theme and font

To further enhance readability of your text editing environment

See the results below.

20. Syncing and back-uping Sublime Text 2 settings and plug-ins with Dropbox

You may want to

  • Save your Sublime Text 2 configuration for which you have spend so many tears to tune it up
  • Sync your Sublime Text 2 configuration across different computers

Here are instructions for syncing and saving Sublime Text 2 settings with Dropbox.

21. HTML / XML tag tools

No one loves XML sit-ups. XML’ish languages where not intended to be written by hand. Sublime Text provides some pain killers on the matter.

Install Tag from Package Control. It comes handy when you are cleaning up those hard tabs….

Select text and then search from Command Palette (CMD + SHIFT + P): Tag: Auto Format Tags on Selection. Your HTML indent and other weirdness will be cleaned up. You can also configure it for different tag writing style preferences.

There is also built-in HTML: Wrap selection with tag command to insert parent or middle level tags in your HTML source.

22. Git

Though Sublime’s file explorer doesn’t support fiel state coloring or context sensitive menu  shortcuts like in Eclipse, you still get some Git commands thru kemayo’s git plug-in (from Package Control, again).

23. Still unresolved

24. More tips

25. Translations

This article is translated to Serbo-Croatian language by Vera Djuraskovic from

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+

Continuous integration server, ghetto style

What it is

Ghetto-CI is a Python script in 145 statements fulfilling your dirty continuous integration needs.

Source code (one file, only 145 statements) is hosted on Github and for your convenience the script is bundled in VVV linting and validation tool package on PyPi. I’d love to see if any other programming language can go more compact from this and still maintain code readability.

What it does

  1. The script must be run on your server periodically
  2. Runs svn update on a source folder (VCS backend easy to customize)
  3. Checks if there are new commits since the last run
  4. Run tests if the source code in fact was changed
  5. See if the tests status since the last run has changed from success to failure or vice versa
  6. Send email notifications to the team that now fecal matter impacts the rotary ventlidation device

Why to use

To improve the quality and cost effectiveness of your little software project, you want to detect code changes breaking your project [automated tests].

You might want to do this without installing 48 MB of Java software on your server. On the other hand, very good SaaS oriented alternatives are tied to public Github repositories. Homebrew shell scripts for tasks like this are nice, but no one wants to read or touch shell scripts written by others.

Ghetto-CI script is mostly self-contained, easy to understand, easy to hack into pieces, for your very own need. It is a solution that scales down. Just toss it on a corner of a server and it will churn happily and keep its owners proud. Ghetto-CI does not you give fancy web interface, statistics, bling bling or even a pony. However, it tells when someone breaks something and it is time for team building via blanket party.


As a prerequisitement you need a working Python 3 command installed on your operating system with virtualenv package. For more detailed instructions see VVV installation manual. Read these instructions especially if you get “SyntaxError: invalid syntax” when running virtualenv command – older Ubuntus ship with virtualenv which is not compatible with Python 3.

If you don’t feel eggy you can just grab the self-contained source file as long as you have plac package also installed for your Python.

Create Python 3 virtualenv and run Ghetto-CI script using Python interpreter configured under this virtualenv:

# We install GhettoCI directly under our UNIX user home folder
cd ~
virtualenv -p python3 vvv-venv
source vvv-venv/bin/activate
pip install vvv

# ghetto-ci now lives in vvv-venv/bin/ghetto-ci

# Running the script, using the Python environment prepared
# to see that everything works (source command above
# has added this to PATH)
ghetto-ci -h


You need to prepare

  • A software repository folder. This must be pre-checked out Subversion repository where ghetto-ci can run svn up command.
  • A command to execute unit tests and such. This command must return process exit code 0 on success. If you don’t bother writing tests, low end alternative is just lint and validate your source code.
  • A file storing the test status. Ghetto-CI status file keeps track whether the last round or tests succeeded or failed. You’ll get email reports only when the test status changed – there is little need to get “tests succeeded” email for every commit.
  • Email server details to send out notifications. Gmail works perfectly if you are in short of SMTP servers.

Example of a command for running continuous integration against /my/svn/repo checkout where bin/test command is used to run the unit tests:

# Will print output to console because email notification details are not given
ghetto-ci /my/svn/repo /tmp/ "cd /my/svn/repo && bin/test"

If the tests status have changed since the last run, or the running fails due to internal error, the command outputs the result. The status file keeps log of the last run.  Exit code 0 indicates that test succeeded.

After Ghetto-CI has been succefully run once by hand you want make it poll the repository regularly. This is can be easily achieved using UNIX cron clock daemon. Create a dummy UNIX user which can checkout and pull updates on the source code. Then create a file /etc/cron.hourly/continuous-integration-tests which will hourly run the tests (Ubuntu example):

sudo -i -u yourunixuser "source vvv-venv/bin/activate && \
  ghetto-ci /my/svn/repo /tmp/ 'cd /my/svn/repo && bin/test'"

Naturally the command to launch the tests is specific to your software project.

On Windows you can accomplish this using any automator provided by your operating system vendor.


You might to use -force -alwaysoutput arguments when doing test runs to see what tests do regardless of software repository timestamp.

You can also evaluate against UNIX command true and false to e.g. test email output:

ghetto-ci -force -alwaysoutput settings here... /repo /tmp/ true

Complex usage example

Below is a real life example , again triggered by Cron job, to poll several SVN repositories which have different test sets to run. We use GMail user to send out the email notifications. This is convenient, as we do not need to care about SMTP environment and we can easily drop this code on any server.

# Example ghetto-ci integration for Plone buildout and custom add-ons.
# Using dummy gmail account for outgoing messages.
# Install VVV under buildout
# Run this file in buildout main folder:
# cd ~/mybuildoutfolder
# src/my-repo/

# The list of persons who will receive resports of test status changes

# Ghetto-CI template command which is run against multiple repos / multiple test commands
# We use localhost 25 as the SMTP server -> assume your UNIX server has postfix
# or something configured... could be servers also here
GHETTOCI="vvv-venv/bin/ghetto-ci \
    -smtpserver \
    -smtpport 465 \
    -smtpuser \
    -smtppassword OMGITISFULLofKITT3NS \
    -receivers \"$RECEIVERS\" \
    -envelopefrom \"Continuous integration service <>\" \

# Note that SVN revision info is folding down in the folders
# so you can target tests to a specific SVN repo subfolder

# Note: eval needs to be used due to shell script quotation mark fuzz

# See that buildout completes (no changes in the external environment, like someone accidentally
# publishing broken packages on PyPI). We actually place buildout.cfg under this SVN repo
# and then just symlink it
eval $GHETTOCI src/my-repo 'bin/buildout'

# Run tests against hospital product
eval $GHETTOCI src/my-repo/Products.Hospital \"bin/test -s Products.Hospital\"

# Run tests against patient product
eval $GHETTOCI src/my-repo/Products.Patient \"bin/test -s Products.Patient\"


plac rocks for command line parsing, especially with Python 3.

The code is pylint valid – and beautiful, Pythonic.

Future tasks

To make this script even more awesome, the following can be considered

  • Using some Python library as the abstraction layer over different version control systems
  • Using more intelligent Python library for the notifications: have email, IRC and Skype notifications (How Skype bots are built nowadays?)
  • Use a proper emailing library. I still believe it is easier to configure one GMail account for SMTP purposes instead of Postfix or Exim. Also GMail nicely collects outgoing messages to log even if email delivery has temporary problems.
  • I would be happy if someone told how to change -smtpport styles options to –smtp-port with plac
  • I would be also happy if someone shows how to tame shell script quotation marks properly
  • All tips to make Python source even more beautiful welcome

The code

__author__ = "Mikko Ohtamaa <>"
__license__ = "WTFPL"

# pylint complains about abstract Python 3 members and ABCMeta, this will be fixed in the future
# pylint: disable=R0201, W0611

# Python imports
from abc import ABCMeta, abstractmethod
from email.mime.text import MIMEText
import pickle
import os
import sys
import subprocess
from smtplib import SMTP_SSL, SMTP 

#: Template used in email notifications
Last commit %(commit)s by %(author)s:


Test output:


def split_first(line, separator):
    Split a string to (first part, remainder)
    parts = line.split(separator)
    return parts[0], separator.join(parts[1:])

def shell(cmdline):
    Execute a shell command.

    :returns: (exitcode, stdout / stderr output as string) tuple

    process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)

    # XXX: Support stderr interleaving
    out, err = process.communicate()

    # :E1103: *%s %r has no %r member (but some types could not be inferred)*
    # pylint: disable=E1103
    out = out.decode("utf-8")
    err = err.decode("utf-8")

    return (process.returncode, out + err)    

class Status:
    Stored pickled CI status of a test run.

    Use Python pickling serialization for making status info persistent.

    def __init__(self):

        #: Set to True of False by automation,
        # but we have a special value for the first run
        # to get output always
        self.test_success = "xxx"
        self.last_commit_id = None

    def read(cls, path):
        Read status file.

        Return fresh status if file does not exist.

        if not os.path.exists(path):
            # Status file do not exist, get default status
            return Status()

        f = open(path, "rb")

            return pickle.load(f)

    def write(cls, path, status):
        Write status file
        f = open(path, "wb")
        pickle.dump(status, f)

class Repo(metaclass=ABCMeta):
    Define interface for presenting one monitored software repository in ghetto-ci.
    def __init__(self, path):
        :param path: Abs FS path to monitored repository
        self.path = path

    def update(self):
        """ Update repo from version control """

    def get_last_commit_info(self):
        Get the last commit status.

        :return tuple(commit id, commiter, message, raw_output) or (None, None, None, raw_output) on error
        return (None, None, None)

class SVNRepo(Repo):
    """ Handle Subversion repository update and last commit info extraction """

    def update(self):
        Run repo update in a source folder
        exitcode, output = shell("svn up %s" % self.path)
        return exitcode == 0, output

    def get_last_commit_info(self):
        Get the last commit status.

        # Get output from svn info
        info, output = self.get_svn_info()

        if not info:
            return (None, None, None, output)

        # Get output from svn log
        log_success, author, log = self.get_svn_log()
        if not log_success:
            return (None, None, None, log)

        return (info["Last Changed Rev"], author, log, output)

    def get_svn_log(self):
        Extract the last commit author and message.

        :return: tuple (success, last commiter, output / last commit message)
        exit_code, output = shell("svn log -l 1 %s" % self.path)

        if exit_code != 0:
            return (False, None, output)

        # ------------------------------------------------------------------------
        # r6101 | xxx | 2012-04-28 15:57:14 +0300 (Sat, 28 Apr 2012) | 1 line

        lines = output.split("\n")
        author_line = lines[1]
        author = author_line.split("|")[1].strip()

        return  (True, author, output)

    def get_svn_info(self):
        Get svn info output parsed to dict
        exit_code, output = shell("svn info %s" % self.path)
        if exit_code != 0:
            return None, output

        data = {}
        for line in output.split("\n"):
            key, value = split_first(line, ":")
            data[key] = value

        return data, output

class Notifier:
    Intelligent spam being. Print messages to stdout and send emai if
    SMTP server details are available.

    def __init__(self, server, port, username, password, receivers, from_address, envelope_from):
        :param server: SMTP server

        :param port: SMTP port, autodetects SSL

        :param username: SMTP credentials 

        :param password: SMTP password

        :param receivers: String, comma separated list of receivers

        :param from_email: Sender's email address

        self.server = server
        self.port = port
        self.username = username
        self.password = password
        self.receivers = receivers
        self.from_address = from_address
        self.envelope_from = envelope_from

    def send_email_notification(self, subject, output):

        Further info:


        :param receivers: list of email addresses

        :param subject: Email subject

        :param output: Email payload

        if not self.receivers:
            raise RuntimeError("Cannot send email - no receivers given")

        if self.port == 465:
            # SSL encryption from the start
            smtp = SMTP_SSL(self.server, self.port)
            # Plain-text SMTP, or opt-in to SSL using starttls() command
            smtp = SMTP(self.server, self.port)

        msg = MIMEText(output, "text/plain")
        msg['Subject'] = subject

        if self.envelope_from:
            msg['From'] = self.envelope_from
            msg['From'] = self.from_address

        # Add visible receivers header
        msg["To"] = self.receivers

        # SMTP never works on the first attempt...

        # SMTP authentication is optional
        if self.username and self.password:
            smtp.login(self.username, self.password)

        # Convert comma-separated sl
        receivers = [email.strip() for email in self.receivers.split(",")]

            smtp.sendmail(self.from_address, receivers, msg.as_string())

    def print_notification(self, subject, output):
        Dump the notification to stdout
        print("-" * len(subject))

    def notify(self, subject, output):
        Notify about the tests status.
        if self.server:
            self.send_email_notification(subject, output)

        self.print_notification(subject, output)

def run_tests(test_command):
    Run testing command.

    Assume exit code = 0 -> test success
    exitcode, output = shell(test_command)
    return (exitcode == 0, output)

# Parsing command line with plac rocks
def main(smtpserver : ("SMTP server address for mail out. Required if you indent to use email notifications.", "option"),
         smtpport : ("SMTP server port for mail out", "option", None, int),
         smtpuser : ("SMTP server username", "option"),
         smtppassword : ("SMTP server password", "option"),
         smtpfrom : ("Notification email From address", "option"),
         envelopefrom : ("Verbose Name <> sender address in outgoing email", "option"),
         receivers : ("Notification email receives as comma separated string", "option"),
         force : ("Run tests regardless if there have been any repository updates", "flag"),
         alwaysoutput : ("Print test run output regardless whether test status has changed since the last run", "flag"),

         repository : ("Monitored source control repository (SVN)", "positional"),
         statusfile : ("Status file to hold CI history of tests", "positional"),
         testcommand : ("Command to run tests. Exit code 0 indicates test success", "positional"),

    A simple continuous integration server.

    Ghetto-CI will monitor the software repository.
    Give a (Subversion) software repository and a test command run test against it.
    Make this command run regularly e.g. using UNIX cron service.    
    You will get email notification when test command status changes from exit code 0.

    For more information see

    notifier = Notifier(server=smtpserver, port=smtpport,
                      username=smtpuser, password=smtppassword, from_address=smtpfrom,
                      receivers=receivers, envelope_from=envelopefrom)

    repo = SVNRepo(repository)    
    status =

    success, output = repo.update()

    # Handle repo update failure
    if not success:
        notifier.notify("Could not update repository: %s. Probably not valid SVN repo?\n" % repository, output)
        return 1

    commit_id, commit_author, commit_message, output = repo.get_last_commit_info()

    # Handle repo info failure
    if not commit_id:
        notifier.notify("Could not get commit info: %s\n%s" % (repository, output), "Check svn info by hand")
        return 1

    # See if repository status has changed
    if commit_id  != status.last_commit_id or force:
        test_success, output = run_tests(testcommand)
        # No new commits, nothing to do
        print("No changes in repo %s" % repository)
        return 0

    # Test run status have changed since last run
    if (test_success != status.test_success) or alwaysoutput:

        notification_body = NOTIFICATION_BODY_TEMPLATE % dict(commit = commit_id, author=commit_author,
            commit_message=commit_message, test_output=output)

        if test_success:
            subject = "Test now succeed for %s" % testcommand
            subject = "Test now fail for %s" % testcommand

        notifier.notify(subject, notification_body)

    # Update persistent test status
    new_status = Status()
    new_status.last_commit_id = commit_id
    new_status.test_success = test_success
    Status.write(statusfile, new_status)

    if test_success:
        return 0
        return 1

def entry_point():
    Enter the via entry_point declaration.

    Handle UNIX style application exit values
    import plac
    exitcode =

if __name__ == "__main__":
Happy weekend! 🙂

\"\" Subscribe to RSS feed Follow me on Twitter Follow me on Facebook Follow me Google+