How to Create a Sublime Text 2 Plugin

Preface: Terminology and Reference Material

The extension model forSublime Text 2is fairly full-featured.

The extension model for Sublime Text 2 is fairly full-featured. There are ways to change the syntaxhttp://net.tutsplus.com/tutorials/python-tutorials/how-to-create-a-sublime-text-2-plugin/?search_index=20 highlighting, the actual chrome of the editor and all of the menus. Additionally, it is possible to create new build systems, auto-completions, language definitions, snippets, macros, key bindings, mouse bindings and plugins. All of these different types of modifications are implemented via files which are organized into packages.

A package is a folder that is stored in your Packagesdirectory. You can access your Packages directory by clicking on the Preferences > Browse Packages… menu entry. It is also possible to bundle a package into a single file by creating a zip file and changing the extension to .sublime-package. We’ll discuss packaging a bit more further on in this tutorial.

Sublime comes bundled with quite a number of different packages. Most of the bundled packages are language specific. These contain language definitions, auto-completions and build systems. In addition to the language packages, there are two other packages: Default and User. The
Default package contains all of the standard key bindings, menu definitions, file settings and a whole bunch of plugins written in Python. The User package is special in that it is always loaded last. This allows users to override defaults by customizing files in their User package.

During the process of writing a plugin, theSublime Text 2 API referencewill be essential.

During the process of writing a plugin, the Sublime Text 2 API referencewill be essential. In addition, the Default package acts as a good reference for figuring out how to do things and what is possible. Much of the functionality of the editor is exposed via commands. Any operation other than typing characters is accomplished via commands. By viewing thePreferences > Key Bindings – Defaultmenu entry, it is possible to find a treasure trove of built-in functionality.

Now that the distinction between a plugin and package is clear, let’s begin writing our plugin.


Step 1 - Starting a Plugin

Sublime comes with functionality that generates a skeleton of Python code needed to write a simple plugin. Select the Tools > New Plugin…menu entry, and a new buffer will be opened with this boilerplate.

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

Here you can see the two Sublime Python modules are imported to allow for use of the API and a new command class is created. Before editing this and starting to create our own plugin, let’s save the file and trigger the built in functionality.

When we save the file we are going to create a new package to store it in. Press ctrl+s (Windows/Linux) orcmd+s (OS X) to save the file. The save dialog will open to the User package. Don’t save the file there, but instead browse up a folder and create a new folder named Prefixr.

  1. Packages/  
  2. …  
  3. - OCaml/  
  4. - Perl/  
  5. - PHP/  
  6. - Prefixr/  
  7. - Python/  
  8. - R/  
  9. - Rails/  
  10. …  

Now save the file inside of the Prefixr folder as Prefixr.py. It doesn’t actually matter what the filename is, just that it ends in .py. However, by convention we will use the name of the plugin for the filename.

Now that the plugin is saved, let’s try it out. Open the Sublime console by pressing ctrl+`. This is a Python console that has access to theAPI. Enter the following Python to test out the new plugin:

  1. view.run_command('example')  

You should see Hello World inserted into the beginning of the plugin file. Be sure to undo this change before we continue.


Step 2 - Command Types and Naming

For plugins, Sublime provides three different types of commands.

  • Text commands provide access to the contents of the selected file/buffer via a View object
  • Window commands provide references to the current window via a Window object
  • Application commands do not have a reference to any specific window or file/buffer and are more rarely used

Since we will be manipulating the content of a CSS file/buffer with this plugin, we are going to use thesublime_plugin.TextCommand class as the basis of our custom Prefixr command. This brings us to the topic of naming command classes.

In the plugin skeleton provided by Sublime, you’ll notice the class:

  1. class ExampleCommand(sublime_plugin.TextCommand):  

When we wanted to run the command, we executed the following code in the console:

  1. view.run_command('example')  

Sublime will take any class that extends one of the sublime_plugin classes
(TextCommandWindowCommand or ApplicationCommand), remove the suffix Command and then convert the CamelCaseinto underscore_notation for the command name.

Thus, to create a command with the name prefixr, the class needs to be PrefixrCommand.

  1. class PrefixrCommand(sublime_plugin.TextCommand):  

Step 3 - Selecting Text

One of the most useful features of Sublime is the ability to have multiple selections.

Now that we have our plugin named properly, we can begin the process of grabbing CSS from the current buffer and sending it to the Prefixr API. One of the most useful features of Sublime is the ability to have multiple selections. As we are grabbing the selected text, we need to write our plug into handle not just the first selection, but all of them.

Since we are writing a text command, we have access to the current view via self.view. The sel() method of the Viewobject returns an iterable RegionSet of the current selections. We start by scanning through these for curly braces. If curly braces are not present we can expand the selection to the surrounding braces to ensure the whole block is prefixed. Whether or not our selection included curly braces will also be useful later to know if we can tweak the whitespace and formatting on the result we getback from the Prefixr API.

  1. braces = False  
  2. sels = self.view.sel()  
  3. for sel in sels:  
  4.     if self.view.substr(sel).find('{') != -1:  
  5.         braces = True  

This code replaces the content of the skeleton run() method.

If we did not find any curly braces we loop through each selection and adjust the selections to the closest closing curly brace. Next, we use the built-in command expand_selection with the to arg set to bracketsto ensure we have the complete contents of each CSS block selected.

  1. if not braces:  
  2.     new_sels = []  
  3.     for sel in sels:  
  4.         new_sels.append(self.view.find('\}', sel.end()))  
  5.     sels.clear()  
  6.     for sel in new_sels:  
  7.         sels.add(sel)  
  8.     self.view.run_command("expand_selection", {"to""brackets"})  

If you would like to double check your work so far, please compare the source to the file Prefixr-1.py in the source code zip file.


Step 4 - Threading

To prevent a poor connection from interrupting other work, we need to make sure that the Prefixr API calls are happening in the background.

At this point, the selections have been expanded to grab the full contents of each CSS block. Now, we need to send them to the Prefixr API. This is a simple HTTP request, which we are going to use the urllib and urllib2 modules for. However, before we start firing off web requests, we need to think about how a potentially laggy web request could affect the performance of the editor. If, for some reason, the user is on a high-latency, or slow connection, the requests to the Prefixr API could easily take a couple of seconds or more.

To prevent a poor connection from interrupting other work, we need to make sure that the Prefixr API calls are happening in the background. If you don’t know anything about threading, a very basic explanation is that threads are a way for a program to schedule multiple sets of code to run seemingly at the same time. It is essential in our case because it lets the code that is sending data to, and waiting for a response from, the Prefixr API from preventing the rest of the Sublime user interface from freezing.


Step 5 - Creating Threads

We will be using the Python threading module to create threads. To use the threading module, we create a new class that extends threading.Thread called PrefixrApiCall. Classes that extendthreading.Thread include a run() method that contains all code to be executed in the thread.

  1. class PrefixrApiCall(threading.Thread):  
  2.     def __init__(self, sel, string, timeout):  
  3.         self.sel = sel  
  4.         self.original = string  
  5.         self.timeout = timeout  
  6.         self.result = None  
  7.         threading.Thread.__init__(self)  
  8.   
  9.     def run(self):  
  10.         try:  
  11.             data = urllib.urlencode({'css'self.original})  
  12.             request = urllib2.Request('http://prefixr.com/api/index.php', data,  
  13.                 headers={"User-Agent""Sublime Prefixr"})  
  14.             http_file = urllib2.urlopen(request, timeout=self.timeout)  
  15.             self.result = http_file.read()  
  16.             return  
  17.   
  18.         except (urllib2.HTTPError) as (e):  
  19.             err = '%s: HTTP error %s contacting API' % (__name__, str(e.code))  
  20.         except (urllib2.URLError) as (e):  
  21.             err = '%s: URL error %s contacting API' % (__name__, str(e.reason))  
  22.   
  23.         sublime.error_message(err)  
  24.         self.result = False  

Here we use the thread __init__() method to set all of the values that will be needed during the web request. The run() method contains the code toset up and execute the HTTP request for the Prefixr API. Since threads operate concurrently with other code, it is not possible to directly return values. Instead we set self.result to the result of the call.

Since we just started using some more modules in our plugin, we must add them to the import statements at the top of the script.

  1. import urllib  
  2. import urllib2  
  3. import threading  

Now that we have a threaded class to perform the HTTP calls, we need to create a thread for each selection. To do this we jump back into the run() method of our PrefixrCommand class and use the following loop:

  1. threads = []  
  2. for sel in sels:  
  3.     string = self.view.substr(sel)  
  4.     thread = PrefixrApiCall(sel, string, 5)  
  5.     threads.append(thread)  
  6.     thread.start()  

We keep track of each thread we create and then call the start() method to start each.

If you would like to double check your work so far, please compare the source to the file Prefixr-2.py in the source code zip file.


Step 6 - Preparing for Results

Now that we’ve begun the actual Prefixr API requests we need toset up a few last details before handling the responses.

First, we clear all of the selections because we modified them earlier. Later we will set them back to a reasonable state.

  1. self.view.sel().clear()  

In addition we start a new Edit object. This groups operations for undo and redo. We specify that we are creating a group for the prefixr command.

  1. edit = self.view.begin_edit('prefixr')  

As the final step, we call a method we will write next that will handle the result of the API requests.

  1. self.handle_threads(edit, threads, braces)  

Step 7 - Handling Threads

At this point our threads are running, or possibly even completed. Next, we need to implement thehandle_threads() method we just referenced. This method is going to loop through the list of threads and look for threads that are no longer running.

  1. def handle_threads(self, edit, threads, braces, offset=0, i=0, dir=1):  
  2.     next_threads = []  
  3.     for thread in threads:  
  4.         if thread.is_alive():  
  5.             next_threads.append(thread)  
  6.             continue  
  7.         if thread.result == False:  
  8.             continue  
  9.         offset = self.replace(edit, thread, braces, offset)  
  10.     threads = next_threads  

If a thread is still alive, we add it to the list of threads to check again later. If the result was a failure, we ignore it, however for good results we call a new replace() method that we’ll be writing soon.

If there are any threads that are still alive, we need to check those again shortly. In addition, it is a nice user interface enhancement to provide an activity indicator to show that our plugin is still running.

  1. if len(threads):  
  2.     # This animates a little activity indicator in the status area  
  3.     before = i % 8  
  4.     after = (7) - before  
  5.     if not after:  
  6.         dir = -1  
  7.     if not before:  
  8.         dir = 1  
  9.     i += dir  
  10.     self.view.set_status('prefixr''Prefixr [%s=%s]' % \  
  11.         (' ' * before, ' ' * after))  
  12.   
  13.     sublime.set_timeout(lambdaself.handle_threads(edit, threads,  
  14.         braces, offset, i, dir), 100)  
  15.     return  

The first section of code uses a simple integer value stored in the variable i to move an = back and forth between two brackets. The last part is the most important though. This tells Sublime to run thehandle_threads()method again, with new values, in another 100 milliseconds. This is just like thesetTimeout() function in JavaScript.

The lambda keyword is a feature of Python that allows us to create a new unnamed, or anonymous, function.

The sublime.set_timeout() method requires a function or method and the number of milliseconds until it should be executed. Without lambda we could tell it we wanted to run handle_threads(), but we would not be able to specify the parameters.

If all of the threads have completed, we don’t need to set another timeout, but instead we finish our undo group and update the user interface to let the user know everything is done.

  1. self.view.end_edit(edit)  
  2.   
  3. self.view.erase_status('prefixr')  
  4. selections = len(self.view.sel())  
  5. sublime.status_message('Prefixr successfully run on %s selection%s' %  
  6.     (selections, '' if selections == 1 else 's'))  

If you would like to double check your work so far, please compare the source to the file Prefixr-3.py in the source code zip file.


Step 8 - Performing Replacements

With our threads handled, we now just need to write the code that replaces the original CSS with the result from the Prefixr API. As we referenced earlier, we are going to write a method called replace().

This method accepts a number of parameters, including the Edit object for undo, the thread that grabbed the result from the Prefixr API, if the original selection included braces, and finally the selection offset.

  1. def replace(self, edit, thread, braces, offset):  
  2.     sel = thread.sel  
  3.     original = thread.original  
  4.     result = thread.result  
  5.   
  6.     # Here we adjust each selection for any text we have already inserted  
  7.     if offset:  
  8.         sel = sublime.Region(sel.begin() + offset,  
  9.             sel.end() + offset)  

The offset is necessary when dealing with multiple selections. When we replace a block of CSS with the prefixed CSS, the length of that block will increase. The offset ensures we are replacing the correct content for subsequent selections since the text positions all shift upon each replacement.

The next step is to prepare the result from the Prefixr API to be dropped in as replacement CSS. This includes converting line endings and indentation to match the current document and original selection.

  1. result = self.normalize_line_endings(result)  
  2. (prefix, main, suffix) = self.fix_whitespace(original, result, sel,  
  3.     braces)  
  4. self.view.replace(edit, sel, prefix + main + suffix)  

As a final step we set the user’s selection to include the end of the last line of the new CSS we inserted, and then return the adjusted offset to use for any further selections.

  1. end_point = sel.begin() + len(prefix) + len(main)  
  2. self.view.sel().add(sublime.Region(end_point, end_point))  
  3.   
  4. return offset + len(prefix + main + suffix) - len(original)  

If you would like to double check your work so far, please compare the source to the file Prefixr-4.py in the source code zip file.


Step 9 - Whitespace Manipulation

We used two custom methods during the replacement process to prepare the new CSS for the document. These methods take the result of Prefixr and modify it to match the current document.

normalize_line_endings() takes the string and makes sure it matches the line endings of the current file. We use the Settings class from the Sublime API to get the proper line endings.

  1. def normalize_line_endings(self, string):  
  2.     string = string.replace('\r\n''\n').replace('\r''\n')  
  3.     line_endings = self.view.settings().get('default_line_ending')  
  4.     if line_endings == 'windows':  
  5.         string = string.replace('\n''\r\n')  
  6.     elif line_endings == 'mac':  
  7.         string = string.replace('\n''\r')  
  8.     return string  

The fix_whitespace() method is a little more complicated, but does the same kind of manipulation, just for the indentation and whitespace in the CSS block. This manipulation only really works with a single block of CSS, so we exit if one or more braces was included in the original selection.

  1. def fix_whitespace(self, original, prefixed, sel, braces):  
  2.     # If braces are present we can do all of the whitespace magic  
  3.     if braces:  
  4.         return ('', prefixed, '')  

Otherwise, we start by determining the indent level of the original CSS. This is done by searching for whitespace at the beginning of the selection.

  1. (row, col) = self.view.rowcol(sel.begin())  
  2. indent_region = self.view.find('^\s+'self.view.text_point(row, 0))  
  3. if self.view.rowcol(indent_region.begin())[0] == row:  
  4.     indent = self.view.substr(indent_region)  
  5. else:  
  6.     indent = ''  

Next we trim the whitespace from the prefixed CSS and use the current view settings to indent the trimmed CSS to the original level using either tabs or spaces depending on the current editor settings.

  1. prefixed = prefixed.strip()  
  2. prefixed = re.sub(re.compile('^\s+', re.M), '', prefixed)  
  3.   
  4. settings = self.view.settings()  
  5. use_spaces = settings.get('translate_tabs_to_spaces')  
  6. tab_size = int(settings.get('tab_size'8))  
  7. indent_characters = '\t'  
  8. if use_spaces:  
  9.     indent_characters = ' ' * tab_size  
  10. prefixed = prefixed.replace('\n''\n' + indent + indent_characters)  

We finish the method up by using the original beginning and trailing white space to ensure the new prefixed CSS fits exactly in place of the original.

  1. match = re.search('^(\s*)', original)  
  2. prefix = match.groups()[0]  
  3. match = re.search('(\s*)\Z', original)  
  4. suffix = match.groups()[0]  
  5.   
  6. return (prefix, prefixed, suffix)  

With the fix_whitespace() method we used the Python regular expression (re)module, so we need to add it to the list of imports at the top of the script.

And with this, we’ve completed the process of writing the prefixr command.The next step it to make the command easy to run by providing a keyboard shortcut and a menu entry.


Step 10 - Key Bindings

Most of the settings and modifications that can be made to Sublime are done via JSON files, and this is true for key bindings. Key bindings are usually OS-specific, which means that three key bindings files will need to be created for your plugin. The files should be named Default (Windows).sublime-keymapDefault (Linux).sublime-keymap and Default (OSX).sublime-keymap.

  1. Prefixr/  
  2. ...  
  3. - Default (Linux).sublime-keymap  
  4. - Default (OSX).sublime-keymap  
  5. - Default (Windows).sublime-keymap  
  6. - Prefixr.py  

The .sublime-keymap files contain a JSON array that contains JSON objects to specify the key bindings. The JSON objects must contain a keys and command key, and may also contain a args key if the command requires arguments. The hardest part about picking a key binding is to ensure the key binding is not already used. This can be done by going to the Preferences > Key Bindings – Default menu entry and searching for the keybinding you wish to use. Once you’ve found a suitably unused binding, add it to your .sublime-keymap files.

  1. [  
  2.     {   
  3.         "keys": ["ctrl+alt+x"], "command""prefixr"   
  4.     }  
  5. ]  

Normally the Linux and Windows key bindings are the same. The cmd key on OS Xis specified by the stringsuper in the .sublime-keymap files. When porting a key binding across OSes, it is common for the ctrlkey onWindows and Linux to be swapped out for super on OS X. This may not, however, always be the most natural hand movement, so if possible try and test your keybindings out on a real keyboard.


Step 11 - Menu Entries

One of the cooler things about extending Sublime is that it is possible to add items to the menu structure by creating .sublime-menu files. Menufiles must be named specific names to indicate what menu they affect:

  • Main.sublime-menu controls the main program menu
  • Side Bar.sublime-menu controls the right-click menu on a file or folder in the sidebar
  • Context.sublime-menu controls the right-click menu on a file being edited

There are a whole handful of other menu files that affect various other menus throughout the interface. Browsing through the Default package is the easiest way to learn about all of these.

For Prefixr we want to add a menu item to the Edit menu and some entries to the Preferences menu for settings. The following example is the JSON structure for the Edit menu entry. I’ve omitted the entries for thePreferences menu since they are fairly verbose being nested a few levels deep.

  1. [  
  2. {  
  3.     "id""edit",  
  4.     "children":  
  5.     [  
  6.         {"id""wrap"},  
  7.         { "command""prefixr" }  
  8.     ]  
  9. }  
  10. ]  

The one piece to pay attention to is the id keys. By specifying the idof an existing menu entry, it is possible to append an entry without redefining the existing structure. If you open the Main.sublime-menu file from the Default package and browse around, you can determine what idyou want to add your entry to.

At this point your Prefixr package should look almost identical to the official version on GitHub.


Step 12 - Distributing Your Package

Now that you’ve taken the time to write a useful Sublime plugin, it is time to get into the hand of other users.

Sublime supports distributing a zip file of a package directory as a simple way to share packages. Simply zip your package folder and change the extension to .sublime-package. Other users may now place this into their Installed Packages directory and restart Sublime to install the package.

Along with easy availability to lots of users, having your package available via Package Control ensures users get upgraded automatically to your latest updates.

While this can certainly work, there is also a package manager forSublime called Package Controlthat supports a master list of packages and automatic upgrades. To get your package added to the default channel, simply host it on GitHuborBitBucket and then fork the channel file (on GitHub, orBitBucket), add your repository and send a pull request. Once the pull request is accepted, your package will be available to thousands of users using Sublime. Along with easy availability to lots of users, having your package available via Package Control ensures users get upgraded automatically to your latest updates.

If you don’t want to host on GitHub or BitBucket, there is a customJSON channel/repository system that can be used to host anywhere, while still providing the package to all users. It also provides advanced functionality like specifying the availability of packages by OS. See the PackageControl pagefor more details.


Go Write Some Plugins!

Now that we’ve covered the steps to write a Sublime plugin, it is time for you to dive in! The Sublime plugin community is creating and publishing new functionality almost every day. With each release, Sublime becomes more and more powerful and versatile. The Sublime Text Forum is a great place to get help and talk with others about what you are building.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值