16. Plugins

Plugins are augmentations to Sublime Text implemented in Python. Plugins are used to add:

to the Sublime Text run-time environment.

In order to be considered a Plugin, a Python file has to be in the ROOT directory of a package (not inside a subdirectory), and it has to have an extension of .py.

Most of the remaining major sections of this documentation are about facilities that are available to both Plugins and Packages.

Note

There is a way to get a Plugin to use .py files that are in a subdirectory below the root of the Package. Many shipped and user-installed Packages demonstrate a clever way of doing this using the Python import system. This is covered in detail in Managing Complexity.

16.1. Creating Commands

New Commands are created by creating classes inside Plugin code by creating a class that inherits from one of these classes:

  • sublime_plugin.TextCommand (instantiated once for each View)

  • sublime_plugin.WindowCommand (instantiated once for each Window)

  • sublime_plugin.ApplicationCommand (instantiated once)

The name of the Command is derived from the class name by converting the name of the class to “snake_case” and removing the trailing “_command” at the end if present. How to do this:

  • lower-case the first letter of the class name

    • InsertTimestampCommand => insertTimestampCommand

  • replace remaining upper-case letters with ‘_’ + lower-case letter

    • insertTimestampCommand => insert_timestamp_command

  • if the resulting name ends with “_command”, then that portion is removed.

    • insert_timestamp_command => insert_timestamp

Thus insert_timestamp is the name of the Command created from the InsertTimestampCommand class name.

In fact, here is the actual Sublime Text code that does this (from sublime_plugin.Command class):

def name(self) -> str:
    """
    Return the name of the command. By default this is derived from the name
    of the class.
    """
    clsname = self.__class__.__name__
    name = clsname[0].lower()
    last_upper = False
    for c in clsname[1:]:
        if c.isupper() and not last_upper:
            name += '_'
            name += c.lower()
        else:
            name += c
        last_upper = c.isupper()
    if name.endswith("_command"):
        name = name[0:-8]
    return name

It is a convention, not a requirement that Command class names end with “...Command”. This practice makes it easier to find Commands (by searches) and recognize Commands with your eyes.

All Commands share the same namespace, so:

  • you can override Sublime Text Commands by creating code that gets loaded after the Command that is getting replaced;

  • consider adopting the practice of preventing “name collisions” by prefixing your Commands with the name or abbreviation of your Package.

All Commands share a common set of methods inherited from class sublime_plugin.Command.

16.1.1. Text Commands

A Command subclassed from sublime_plugin.TextCommand is the only way to get an Edit object. Edit objects are required to make changes to the Buffer attached to the View the Command was invoked from. Each View object has exactly ONE instance of the Edit class. The self.view property contains a reference to the View from which the Command was invoked. (self is the 1st argument passed to the Command when it is run.) One Command object is created for each View, each of which has a view property referencing that View object.

It is important that making your Command inherit from sublime_plugin.TextCommand only be done when:

  • your Command needs access to the contents of that View’s buffer,

  • your Command needs to change the contents of that View’s buffer,

  • your Command needs access to the particular View that the Command was invoke from.

Reason: Every time the command is executed, it is bracketed internally by calls to view.begin_edit() and view.end_edit(), which incur overhead that creates an “undo” object to undo changes in the Buffer with Edit ‣ Undo ...:

# From ``class TextCommand``'s ``run_()`` method:

edit = self.view.begin_edit(edit_token, self.name(), args)
try:
    return self.run(edit, **args)   # <<<-- your TextCommand command is run here.
finally:
    self.view.end_edit(edit)

Thus, if your Command does not have one of the above needs, it is recommended that you instead make your Command inherit from one of the other ...Command classes mentioned above.

16.1.1.1. Types of Views

Views are used in Sublime Text anywhere text can be edited, and this includes in Sublime Text’s version of dialog boxes: Panels and Overlays. Sometimes you want a TextCommand to play a role in a particular View in a Panel (e.g. Find-in-Files Panel’s “Where” textbox), and sometimes you don’t. In a TextCommand, you can easily tell whether the View is connected to a document (or display output such as Find Results) or is a part of a Panel or Overlay. The following methods within the View class will help you determine the nature of a View in your Commands. Hint: a View not connected to a Sheet is part of a Panel or Overlay.

def sheet_id(self) -> int:
    """
    :returns:
        The ID of the `Sheet` for this `View`, or ``0`` if not part of any sheet.
    """
    return sublime_api.view_sheet_id(self.view_id)

def sheet(self) -> Optional[Sheet]:
    """
    :returns: The `Sheet` for this view, if displayed in a sheet.
    """
    return make_sheet(self.sheet_id())

def element(self) -> Optional[str]:
    """
    :returns:
        ``None`` for normal views that are part of a `Sheet`. For views that
        comprise part of the UI a string is returned from the following list:

        * ``"console:input"``                - Console input.
        * ``"goto_anything:input"``          - Input for Goto Anything Overlay.
        * ``"command_palette:input"``        - Input for Command Palette Overlay.
        * ``"find:input"``                   - Input for Find Panel.
        * ``"incremental_find:input"``       - Input for Incremental-Find Panel.
        * ``"replace:input:find"``           - Find input for Replace Panel.
        * ``"replace:input:replace"``        - Replace input for Replace Panel.
        * ``"find_in_files:input:find"``     - Find input for Find-in-Files Panel.
        * ``"find_in_files:input:location"`` - Where input for Find-in-Files Panel.
        * ``"find_in_files:input:replace"``  - Replace input for Find-in-Files Panel.
        * ``"find_in_files:output"``         - Output panel for Find-in-Files (buffer or output panel).
        * ``"input:input"``                  - Input for Input panel.
        * ``"exec:output"``                  - Output for exec command.
        * ``"output:output"``                - A general output panel.

        The console output, indexer status output and license input controls
        are not accessible via the API.
    """
    e = sublime_api.view_element(self.view_id)
    if e == "":
        return None
    return e

16.1.2. Window Commands

Like TextCommand objects, sublime_plugin.WindowCommand objects know what window under which they are being executed by way of their window property.

16.1.3. Application Commands

There is only ever one instance of the Application object. All references to it refer to the same object. Instances of the sublime_plugin.ApplicationCommand class are used to do things that are not specific to a View or Window, e.g. changing global application settings such as font size.

16.1.4. All Commands

The Command is “carried out” by calling its run() method, so at minimum, each Command you create will need to have a run() method defined.

Note

If your Python code is failing to compile, Sublime Text will not load it. This is also true if you pass erroneous arguments to certain functions. If a menu item is “attached” to your command, it will be disabled (grayed out).

Example:

This code in an example Plugin TextCommand compiles and the menu items that are “attached” to it are enabled.

self.view.add_regions("test", rgn_list, "region.orangish", "bookmark",
    flags=sublime.DRAW_EMPTY | sublime.DRAW_NO_FILL)

But while the code below compiles, it is assigning a string to an unrecognized keyword argument, and because of this, any menu items that are “attached” to it will be disabled (grayed out).

self.view.add_regions("test", rgn_list, wrong_name="region.orangish", "bookmark",
    flags=sublime.DRAW_EMPTY | sublime.DRAW_NO_FILL)

16.2. Other Command Methods

See https://www.sublimetext.com/docs/api_reference.html#sublime_plugin.TextCommand and don’t forget to also click on the Command link to show the inherited methods.

16.2.1. Inherited Methods

Any inherited method can be overridden, and are meant to be when their default behavior needs to be changed.

One example of this is the is_enabled() method which returns a Boolean indicating whether the command should be run-able at this time. An example of such an override definitions is:

def is_enabled(self) -> bool:
    scope = self.view.scope_name(self.view.sel()[-1].b).rstrip()
    print(f'Scope=[{scope}]')
    return ('source' in scope and 'comment' in scope)

Note that this overrides that method in the Command class itself. Observe that the method determines if “source” and “comment” are in the scope of the cursor and if so, then it is enabled. Otherwise not. So if you have this command attached to a Menu Item, for instance, if your cursor is not in a comment in a source file, that menu item will be disabled. If it is, then you can run it.

is_enabled() has the capability to block or allow running from:

  • menu (enables or disables menu item);

  • keymap (selects or blocks key combination(s) mapped to it);

  • Command Palette (hides or makes visible Command Palette items);

  • running through window.run_command('your_cmd_name') or view.run_command('your_cmd_name').

16.3. More on Text Commands

When you are executing a Command (e.g. via a menu item or key combination) intended to use or change the contents of the text Buffer in some way, the only way to do so is with a Text Command. You create a Text Command by creating a class that inherits from sublime_plugin.TextCommand. At minimum, it will need a run() method that accepts self and edit as arguments. Example:

import sublime
import sublime_plugin

class InsertTimestampCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        pass

Once your run() method is in place, you will need to understand the tools available to you within that method.

Firstly, self.view will always be attached to the View that had focus when your Command was executed. Reason: each View carries with it an instantiation of each available Command, and each Command so instantiated thereby knows the View it is attached to.

Note

If your command’s run() method will not edit the buffer, then what you normally want to inherit from is a sublime_plugin.WindowCommand or sublime_plugin.ApplicationCommand. Reason: a sublime_plugin.TextCommand’s run() method is bracketed by a call to

  • view.begin_edit() and

  • view.end_edit()

which make the necessary memory allocations and other actions to make it possible to UNDO that edit later if needed. If you are not going to edit the buffer, then putting actions in a sublime_plugin.TextCommand causes a waste of CPU overhead and memory resources. You can tell if a View is between the begin_edit() and end_edit() by calling view.is_in_edit().

16.3.1. Some Data Types You Need to Know About

The following are some data types you will need to know well because you will need to use them to access and modify text in Text Commands.

16.3.2. What Happens Inside the run() Method

Within a Text Command’s run() method:

  • the TextCommand object is 1st argument, usually self,

  • the View’s Edit object is the 2nd argument usually edit, and

  • the following are tools you will likely use often:

    • The applicable View is self.view.

    • The list of Selection Regions is self.view.selection...

    • ...though it is probably meant to be retrieved by rgn_list = self.view.sel() because self.view.selection is not in the documentation. Note: the order of this list is always from the top of the file down, and the regions never overlap.

    • The first Selection Region (caret if it is empty) is rgn_list[0]

    • Iterate through all Selection Regions by for rgn in rgn_list:

    • selected_text = self.view.substr(rgn)

    • Regions carry 2 Point objects: rgn.a and rgn.b.

    • Let’s say we have executed pt = rgn.a.

    • char_at_pt = self.view.substr(pt)

    • Is Region empty? rgn.empty()

    • The smaller of a and b is rgn.begin()

    • The larger of a and b is rgn.end()

    • The Point where the caret is always: rgn.b, even when rgn.b < rgn.a.

    • Number of characters represented: rgn.size()

    • Determine context of a Point: scope_name = self.view.scope_name(pt) which can tell whether an edit may be appropriate for where the caret is. (If the Command is being executed from a Menu item, the “context” filter available in Key Bindings may not be available in the menu, so you can still find out programmatically about the nature of the position of the caret and thus make the judgement programmatically.)

    • Does a Point match a context selector name? ok_to_edit = self.view.match_selector(pt, scope_name)

    • Does Region contain Point? rgn.contains(pt)

    • file_name = self.view.file_name() (None if it doesn’t exist on disk.)

    • Change target file: self.view.retarget(new_file_name)

    • Find Buffer’s encoding: enc_name = self.view.encoding()

    • Change Buffer’s encoding: self.view.set_encoding(enc_name)

    • Number of chars in file: char_count = self.view.size()

    • View’s private settings: private_settings = self.view.settings() (This object behaves a bit like a dictionary, and a dictionary can be retrieved from it using settings.to_dict(). These, however, are only a copy, thus changing them does nothing—a call to settings.set() or settings.erase() must be made to actually change a value for the View. Note these are not the application’s settings you get to via Preferences ‣ Settings, but are an entirely different data set application to Views.

    • Get all lines involved: lines = self.view.lines(rgn)

    • Get line where a point is: line = self.view.line(rgn|pt)

    • Same but with newline: line = self.view.full_line(rgn|pt)

    • Word or phrase: word = self.view.word(rgn|pt)

    • Attributes of a Point: pt_classif = self.view.classify(pt)

    • Region that is visible in View: vis_rgn = self.view.visible_region()

    • Is Region visible in View? visible = vis_rgn.contains(rgn.a)

    • Number of changes made in this View: chg_count = self.view.change_count()

16.3.2.1. Editing the Buffer

Ways to edit the Buffer:

  • Delete any selected text by self.view.erase(edit, rgn)

  • Replace any selected text by self.view.replace(edit, rgn, new_text) (rgn can be empty)

  • char_count_inserted = self.view.insert(edit, pt, new_text) (char_count_inserted may differ from the provided text due to tab translation.)

You can edit any part of the Buffer by using existing Region and Point objects or creating new ones, and doing things with them.

16.3.2.2. Change Position of Caret

A retrieved Region (e.g. via rgn = rgn_list[0] is ONLY A COPY of the current selection Region, so changing its values DOES NOT change the caret in the View. However, performing edits through the view DOES change where the caret is, and its new location can be retrieved by:

rgn_list = self.view.sel()
new_rgn = rgn_list[0]
new_caret_pt = new_rgn.b

Importantly: rgn_list itself has methods by which you can set any number of new regions in it. But to merely return to having 1 caret and setting its position:

rgn_list = self.view.sel()
saved_pos = rgn_list[0]
# some editing here
rgn_list.clear()
rgn_list.add(sublime.Region(saved_pos))

rgn_list.add_all(new_list_of_regions) can be used to establish a set of new regions with one call.

16.3.2.3. Rows and Columns

If a row or column is important to your TextCommand (both are zero-based):

  • row, col = self.view.rowcol(pt) # Clamped to EOF.

  • row, col = self.view.rowcol_utf8(pt) # Clamped to EOF.

  • row, col = self.view.rowcol_utf16(pt) # Clamped to EOF.

  • pt = self.view.text_point(row, col[, clamp_column=True]) # Clamped to EOF.

  • pt = self.view.text_point_utf8(row, col[, clamp_column=True]) # Clamped to EOF.

  • pt = self.view.text_point_utf16(row, col[, clamp_column=True]) # Clamped to EOF.

where col is the number of Unicode characters to advance past the beginning of row, and clamp_column is whether col should be restricted to valid values for the given row.

16.3.2.4. Finding New Region(s)

You can search the Buffer using the following powerful View methods.

16.3.2.4.1. sublime.FindFlags

Two of the API functions below use OR-ed FindFlags bits, which can be accessed using either of the first two Python expressions:

sublime.LITERAL    == sublime.FindFlags.NONE       == 0x00
sublime.LITERAL    == sublime.FindFlags.LITERAL    == 0x01 (literal == not regex)
sublime.IGNORECASE == sublime.FindFlags.IGNORECASE == 0x02
sublime.WHOLEWORD  == sublime.FindFlags.WHOLEWORD  == 0x04
sublime.REVERSE    == sublime.FindFlags.REVERSE    == 0x08 (search backwards)
sublime.WRAP       == sublime.FindFlags.WRAP       == 0x10 (continue at top after EOF)

Here is a compact list of API functions covered below:

  • first_rgn = view.find(regex_str, start_pt, ored_find_flags), and

  • rgn_list  = view.find_all(regex_str, ored_find_flags, fmt, extractions, within)

  • rgn_list  = view.find_by_selector(sel_name)

16.3.2.4.2. view.find()
view.find(
        pattern : str,
        start_pt: Point,
        flags   : int = sublime.FindFlags.NONE
        ) -> Region

Parameters:

pattern:

The regex or literal pattern to search by.

start_pt:

The Point to start searching from.

flags:

Controls various behaviors of find. See sublime.FindFlags.

Returns:

The first Region matching the provided pattern.

16.3.2.4.3. view.find_all()
view.find_all(
        pattern    : str,
        flags      : int = sublime.FindFlags.NONE,
        fmt        : Optional[str] = None,
        extractions: Optional[list[str]] = None,
        within     : Optional[Union[sublime.Region, list[sublime.Region]]] = None
        ) -> list[sublime.Region]

Find all occurrences of pattern in View Buffer based on flags within the range(s) specified by within if specified.

If the fmt string and extractions list (normally empty) are both provided, a Regex-find-and-replace operation is done with the fmt string and each formatted result is appended to the extractions list. The fmt argument does nothing unless extractions is also provided.

Parameters:

pattern:

The regex or literal pattern to search by. If FindFlags.LITERAL is not part of flags then it is a Regex.

flags:

Controls various behaviors of find. See sublime.FindFlags.

fmt:

When not None, this is a Regex Replacement String with special syntax allowing you to use parts of what was found in the formatted output. All matches in the extractions list will be formatted with the provided format string. Numbered and named backreferences to the parts of the matched string as $& (whole matched string), $1 (1st capture group), $2 (2nd capture group), .. ${99}, etc. are fully supported.

See Boost Replace String Syntax for complete details. (Numbered backreferences like \1, \2, etc. still work, but have been deprecated.)

extractions:

An optionally provided (normally empty) list to place the formatted contents of the find results into. This list is only populated (appended to) if the fmt string was also supplied.

within:

(4181) either sublime.Region or list[sublime.Region]. When not None, searching is limited to within the provided region(s).

Returns:

All (non-overlapping) Regions matching the pattern.

16.3.2.4.4. view.find_by_selector()
find_by_selector(
        selector: str
        ) -> list[sublime.Region]

Find all Regions in the Buffer matching the given selector.

Returns:

The list of matched Regions.

16.3.2.5. Running Other Commands

You can run other commands by: self.view.run_command(cmd_name[, {args}]).

16.3.3. Further Reading

There are hundreds of other things you can do, which are beyond the scope of this document, but you can learn a lot more by visiting https://www.sublimetext.com/docs/api_reference.html#sublime_plugin.TextCommand

Also, the following list offers a comprehensive Regex reference:

16.4. Adding Python Packages

Sometimes a Plugin you are developing might need to import from a Python package that is not already in the installed environment.

Todo

Fill in how to depend on another Package

16.4.1. Further Reading

https://forum.sublimetext.com/t/depending-on-an-extra-module-in-a-plugin/917

16.5. Example Basic Plugin with Package Settings

Note: this example is 95% plumbing code. The meat of a plugin is in commands, and for the sake of completeness, one example command is shown at the end.

import sublime
import sublime_plugin


# =========================================================================
# Configuration
# =========================================================================

module_path, _ = os.path.splitext(os.path.realpath(__file__))
_, submodule_name = os.path.split(module_path)
package_name = __package__
this_module_name = f'{package_name}.{submodule_name}'
del _, module_path, submodule_name

# Track on-settings-changed listener.
_cfg_on_settings_chgd_listener_id = '_bp_settings_changed_tag'

# Setting Names
_cfg_stg_name__font_size = 'font_size'
_cfg_stg_name__debugging = 'debugging'

# =========================================================================
# Plugin Definitions
# =========================================================================


def bp_setting(key):
    """
    Get a package setting from a cached settings object.
    "bp" stands for "Basic Plugin".  You can any prefix, or none at all.
    This function expects the following objects to already exist:

    - ``bp_setting.obj``      a ``sublime.Settings`` object (looks like a dictionary)
    - ``bp_setting.default``  a dictionary object with named default values

    :param key:  name of setting whose value will be returned

    """
    assert bp_setting.default is not None, '`bp_setting.default` must exist before calling `bp_setting()`.'
    assert bp_setting.obj is not None, '`bp_setting.obj` must exist before calling `bp_setting()`.'
    default = bp_setting.default.get(key, None)
    return bp_setting.obj.get(key, default)


"""
Establish default settings to be used in case the end user deletes any settings.
``bp_setting.default`` dictionary object is needed by ``bp_setting()``
"""
bp_setting.default = {
    _cfg_stg_name__font_size: 10,
    _cfg_stg_name__debugging: False
}


def _load_cached_settings():
    """
    Load the settings file; every time you invoke ``sublime.load_settings()`` with
    the same settings file, you get the same object back.  This object can be saved
    to provide a settings cache for speedier access during package operation, or
    it can be reloaded as you need.
    """
    pc_setting.obj = sublime.load_settings(_cfg_pkg_settings_file)


def _on_pkg_settings_change():
    """
    Do now with the settings what you would do with them whenever the package settings
    file changes.
    """
    _load_cached_settings()


def _on_pkg_settings_change():
    """
    A function that does something with settings; code here would load the
    settings file, or otherwise access some sort of globally cached one, and then
    do something with said setting. Here we are just printing the value of the
    setting out.

    NOTE:

    As a settings listener target, this gets invoked any time any setting
    changes, so part of your logic here (if it matters) is to check and see
    if the setting you care about is the one that changed. Usually does not
    matter though.
    """
    _load_cached_settings()
    print(f"The configured font size is {bp_setting(_cfg_stg_name__font_size)}")


def plugin_loaded():
    """
    Event Hook:

    This gets executed by Sublime Text once after your plugin is loaded and the API is
    fully ready to go, as well as every time your plugin is saved (even when there
    are no changes).  This is a good place to initialize things like a settings file
    listener, for example. */
    """
    global _cfg_pkg_name
    _establish_default_settings()
    _load_cached_settings()

    # Indicate an interest in when settings change; the key here is important;
    # give it something unique to you (maybe namespace with your plugin name);
    # this is needed to remove the listener.
    bp_setting.obj.add_on_change(_cfg_on_settings_chgd_listener_id, _on_pkg_settings_change)

    # Announce if `debugging`.
    debugging = bp_setting(_cfg_stg_name__debugging)
    if debugging:
        print(f'{_cfg_pkg_name} loaded.')


def plugin_unloaded():
    """
    Event Hook:

    This gets executed by Sublime Text when your plugin is unloaded, which also
    happens when you save it and it reloads, and also when the package that it's in
    is disabled.  Here is a good place to make sure that you don't leak the settings
    listener.
    """
    # Get the settings object and then clear the listener away; this is done via
    # the API and the key that you used to register the listener.
    if bp_setting.obj:
        bp_setting.obj.clear_on_change(_cfg_on_settings_chgd_listener_id)

    # Announce if `debugging`.
    debugging = bp_setting(_cfg_stg_name__debugging)
    if debugging:
        print(f'{_cfg_pkg_name} unloaded.')


class MessageBoxCommand(sublime_plugin.WindowCommand):
    """
    This is only an example.  This is where the meat of the plugin would
    be implemented:  in commands.  Create commands that inherit from
    ``sublime_plugin.TextCommand`` to interact with the View and its Buffer.
    """
    def run(self, msg):
        sublime.message_dialog(msg)

16.6. Generating Reports

You can generate reports into a private view that “does not try to be a file”, i.e. the View does not try to coax the user to save it when it is closed (similar to the Find Results View generated by the Find-in-Files “Find” operation). The results can be saved, but Sublime Text is just as happy to simply close the View without prompting the user to save it.

The answer to this lies in the View class’ set_scratch(True) method.

Here is an example from the OverrideAudit package’s /lib/output_view.py file. The syntax argument, if provided, must be the path to a syntax file, e.g. Packages/pkg_name/path/to/YourSyntax.sublime-syntax.

def new_scratch_view(window, title, syntax=None):
    """
    Create a new view in the given window, giving it a name and an optional
    syntax.
    """
    view = window.new_file()
    view.set_scratch(True)
    view.set_name(title)

    if syntax is not None:
        view.assign_syntax(syntax)

    return view