โ† Back to DevBytes

Sublime Text Extensions/Plugins: Complete Guide

Introduction to Sublime Text Plugins

Sublime Text is one of the most popular code editors among developers, prized for its speed, minimal interface, and powerful extensibility. At the heart of this extensibility lies the plugin system, which allows developers to customize nearly every aspect of the editor. Sublime Text plugins are written in Python and can range from simple snippets to complex language servers and build systems.

Whether you want to automate repetitive tasks, add support for a new programming language, or integrate external tools into your workflow, understanding how to build Sublime Text plugins is an invaluable skill. This guide walks you through everything from the basics of plugin architecture to advanced techniques and best practices.

What Is a Sublime Text Plugin?

A Sublime Text plugin is a Python package that extends the functionality of the editor. Plugins can define commands, event listeners, syntax definitions, completions, snippets, and more. They run inside Sublime Text's embedded Python interpreter (Python 3.3 in Sublime Text 3, and Python 3.8 in Sublime Text 4).

Plugins are distributed as packages, which are essentially folders containing Python files, metadata, and optional resources like menus, key bindings, and settings. A package can be a single .py file or a complex directory structure with submodules.

Key Components of a Plugin

Why Plugins Matter

Sublime Text ships with a robust set of features, but every developer's workflow is unique. Plugins bridge the gap between a generic editor and a personalized development environment. Here are some reasons why plugins matter:

Setting Up Your Development Environment

Before writing your first plugin, you need to set up a proper development environment. Sublime Text makes this straightforward since plugins are just Python files placed in specific directories.

Locating the Packages Directory

All plugins live in the Packages directory. You can find it by selecting Preferences > Browse Packages from the menu. On most systems, the path is:

Enabling Debug Logging

During development, it is helpful to enable debug logging so you can see output from your plugin. Open the Sublime Text console with Ctrl+` (or Cmd+` on macOS) and enter:

sublime.log_commands(True)
sublime.log_input(True)

This will print every command and input event to the console, which is invaluable for debugging.

Writing Your First Plugin

Let us create a simple plugin that inserts a timestamp at the current cursor position. This is a classic beginner example that demonstrates the core concepts of plugin development.

Creating the Plugin File

Navigate to Tools > Developer > New Plugin in the menu. Sublime Text will generate a template file. Save it as timestamp.py inside a new folder called MyPlugins in your Packages directory.

import sublime
import sublime_plugin
from datetime import datetime


class InsertTimestampCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        for region in self.view.sel():
            self.view.insert(edit, region.begin(), timestamp)

Let us break down this code:

Running the Plugin

To run your new command, open the console and type:

view.run_command("insert_timestamp")

You should see a timestamp inserted at your cursor position. To make this more convenient, let us bind it to a keyboard shortcut.

Adding a Key Binding

Create a file named Default (Windows).sublime-keymap, Default (OSX).sublime-keymap, or Default (Linux).sublime-keymap inside your plugin folder. Add the following content:

[
    {
        "keys": ["ctrl+alt+t"],
        "command": "insert_timestamp"
    }
]

Now pressing Ctrl+Alt+T (or Cmd+Alt+T on macOS) will insert a timestamp.

Understanding Command Types

Sublime Text provides three primary command types, each suited for different use cases.

TextCommand

A TextCommand operates on the current view. It receives an edit object and has access to self.view. Use this when you need to read or modify the contents of a file.

class UpperCaseSelectionCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        for region in self.view.sel():
            if not region.empty():
                selected_text = self.view.substr(region)
                self.view.replace(edit, region, selected_text.upper())

WindowCommand

A WindowCommand operates on a window rather than a specific view. It has access to self.window and is useful for commands that open files, show panels, or manage layouts.

class OpenNewFileCommand(sublime_plugin.WindowCommand):
    def run(self):
        new_view = self.window.new_file()
        new_view.set_name("Untitled.txt")
        new_view.run_command("insert_snippet", {"contents": "// New file created by plugin\n"})

ApplicationCommand

An ApplicationCommand is the most general type. It does not have a built-in view or window attribute. Use it for global actions that are not tied to a specific context.

class QuitAllCommand(sublime_plugin.ApplicationCommand):
    def run(self):
        for window in sublime.windows():
            window.run_command("close_window")

Working with the Sublime Text API

The Sublime Text API is the interface through which plugins interact with the editor. Understanding the key objects and methods is essential for building powerful plugins.

The sublime Module

The sublime module provides top-level functions and constants. Some commonly used functions include:

# Get the active window
window = sublime.active_window()

# Get settings
settings = sublime.load_settings("Preferences.sublime-settings")
font_size = settings.get("font_size", 10)

# Show a status message
sublime.status_message("Plugin loaded successfully")

# Display an error dialog
sublime.error_message("Something went wrong!")

# Set a timeout (runs once after delay in milliseconds)
sublime.set_timeout(lambda: print("Delayed execution"), 2000)

# Run on the main thread (useful for UI updates from worker threads)
sublime.set_timeout_async(lambda: print("Running async"), 0)

The View Object

The View object represents a single buffer. Here are some essential methods:

view = sublime.active_window().active_view()

# Get the file name
file_name = view.file_name()

# Get the entire buffer content
full_text = view.substr(sublime.Region(0, view.size()))

# Get text in a specific region
region = sublime.Region(0, 100)
text = view.substr(region)

# Find text using regex
regions = view.find_all(r"\bdef\s+\w+\b")

# Get the current cursor position
sel = view.sel()[0]
row, col = view.rowcol(sel.begin())

# Get the word at the cursor
word_region = view.word(sel.begin())
word = view.substr(word_region)

# Get the scope at a position
scope = view.scope_name(sel.begin())

# Insert, replace, and erase text (requires edit object)
view.insert(edit, 0, "// Header\n")
view.replace(edit, region, "new text")
view.erase(edit, region)

The Window Object

The Window object represents an editor window. Key methods include:

window = sublime.active_window()

# Open a file
window.open_file("/path/to/file.py")

# Show a quick panel (selection list)
items = ["Option 1", "Option 2", "Option 3"]
window.show_quick_panel(items, lambda index: print(f"Selected: {index}"))

# Show an input panel
window.show_input_panel(
    "Enter your name:",
    "default value",
    lambda text: print(f"You entered: {text}"),
    None,
    None
)

# Get all open views
for view in window.views():
    print(view.file_name())

# Show output panel
panel = window.create_output_panel("my_panel")
panel.run_command("append", {"characters": "Hello from plugin!"})
window.run_command("show_panel", {"panel": "output.my_panel"})

Event Listeners

Event listeners allow your plugin to respond automatically to editor events. By subclassing sublime_plugin.EventListener, you can hook into a wide variety of lifecycle events.

Common Event Hooks

class MyEventListener(sublime_plugin.EventListener):

    def on_load(self, view):
        """Called when a file finishes loading."""
        print(f"Loaded: {view.file_name()}")

    def on_save(self, view):
        """Called after a file is saved."""
        print(f"Saved: {view.file_name()}")

    def on_modified(self, view):
        """Called when the view is modified."""
        pass

    def on_selection_modified(self, view):
        """Called when the selection changes."""
        sel = view.sel()[0]
        if not sel.empty():
            word = view.substr(view.word(sel.begin()))
            sublime.status_message(f"Selected word: {word}")

    def on_activated(self, view):
        """Called when a view gains input focus."""
        print(f"Activated: {view.name()}")

    def on_close(self, view):
        """Called when a view is closed."""
        print(f"Closed: {view.file_name()}")

    def on_pre_save(self, view):
        """Called before a file is saved. You can modify the buffer here."""
        pass

Practical Example: Auto-Strip Trailing Whitespace on Save

import sublime
import sublime_plugin
import re


class StripTrailingWhitespaceListener(sublime_plugin.EventListener):
    def on_pre_save(self, view):
        if not view.settings().get("auto_strip_whitespace", True):
            return

        edit = view.begin_edit("strip_whitespace")
        try:
            regions = view.find_all(r"[ \t]+$")
            # Remove overlapping regions in reverse to preserve positions
            for region in reversed(regions):
                view.erase(edit, region)
        finally:
            view.end_edit(edit)
        sublime.status_message("Stripped trailing whitespace")

Note that in Sublime Text 4, the begin_edit and end_edit pattern has been simplified. You can use TextCommand internally for buffer modifications, which is the recommended approach:

import sublime
import sublime_plugin


class StripTrailingWhitespaceListener(sublime_plugin.EventListener):
    def on_pre_save(self, view):
        if view.settings().get("auto_strip_whitespace", True):
            view.run_command("strip_trailing_whitespace")


class StripTrailingWhitespaceCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        regions = self.view.find_all(r"[ \t]+$")
        for region in reversed(regions):
            self.view.erase(edit, region)

Settings and Configuration

Plugins should be configurable so users can customize behavior without editing source code. Sublime Text uses JSON settings files for this purpose.

Creating Plugin Settings

Create a file named MyPlugin.sublime-settings in your plugin folder:

{
    "timestamp_format": "%Y-%m-%d %H:%M:%S",
    "insert_on_new_line": false,
    "excluded_file_types": ["markdown", "plain text"]
}

Reading Settings in Your Plugin

import sublime
import sublime_plugin
from datetime import datetime


class InsertTimestampCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        settings = sublime.load_settings("MyPlugin.sublime-settings")
        fmt = settings.get("timestamp_format", "%Y-%m-%d %H:%M:%S")
        timestamp = datetime.now().strftime(fmt)

        excluded = settings.get("excluded_file_types", [])
        syntax = self.view.settings().get("syntax", "")
        for excluded_type in excluded:
            if excluded_type in syntax:
                sublime.status_message("Timestamp disabled for this file type")
                return

        for region in self.view.sel():
            self.view.insert(edit, region.begin(), timestamp)

Listening for Settings Changes

If your plugin caches settings, you should listen for changes so users see updates immediately:

settings = sublime.load_settings("MyPlugin.sublime-settings")
settings.add_on_change("my_plugin_reload", lambda: print("Settings changed!"))

Adding Autocompletions

Providing autocompletions is one of the most useful features a plugin can offer. Sublime Text supports two approaches: completion files and programmatic completions.

Static Completions File

Create a .sublime-completions file in your plugin folder:

{
    "scope": "source.python",
    "completions": [
        "def ",
        "class ",
        "import ",
        "from ",
        "if __name__ == \"__main__\":",
        "print()",
        "lambda "
    ]
}

Dynamic Completions with EventListener

For more sophisticated completions, use the on_query_completions event:

import sublime
import sublime_plugin


class MyCompletionsListener(sublime_plugin.EventListener):
    COMPLETIONS = [
        ("function\tmy_plugin", "function ${1:name}(${2:args}):\n\t${0:pass}"),
        ("class\tmy_plugin", "class ${1:Name}:\n\t\"\"\"${2:docstring}\"\"\"\n\t${0:pass}"),
        ("logger\tmy_plugin", "logger = logging.getLogger(__name__)"),
    ]

    def on_query_completions(self, view, prefix, locations):
        # Only provide completions for Python files
        if not view.match_selector(locations[0], "source.python"):
            return []

        return self.COMPLETIONS

Each completion is a tuple of (trigger, snippet). The trigger can include a label after a tab character for display in the autocomplete popup.

Creating Syntax Definitions

Syntax definitions tell Sublime Text how to tokenize and highlight files. In modern Sublime Text, syntax definitions are written in the .sublime-syntax YAML format.

Basic Syntax Definition Example

Create a file named MyLang.sublime-syntax:

%YAML 1.2
---
name: MyLang
file_extensions: [myl, mylang]
scope: source.mylang
contexts:
  main:
    - match: \b(if|else|while|for|return|function)\b
      scope: keyword.control.mylang
    - match: '"'
      push: string
    - match: '#.*$'
      scope: comment.line.mylang
    - match: \b\d+\b
      scope: constant.numeric.mylang
    - match: \b[A-Z][a-zA-Z0-9_]*\b
      scope: entity.name.class.mylang

  string:
    - meta_scope: string.quoted.double.mylang
    - match: '\\.'
      scope: constant.character.escape.mylang
    - match: '"'
      pop: true

This definition handles keywords, strings, comments, numbers, and class names. The push and pop directives manage context stacks for nested constructs like strings.

Adding Menu Items and Command Palette Entries

To make your plugin discoverable, you should add entries to menus and the command palette.

Command Palette Entry

Create a file named Default.sublime-commands in your plugin folder:

[
    {
        "caption": "My Plugin: Insert Timestamp",
        "command": "insert_timestamp"
    },
    {
        "caption": "My Plugin: Strip Trailing Whitespace",
        "command": "strip_trailing_whitespace"
    }
]

Context Menu Entry

Create a file named Context.sublime-menu:

[
    {
        "caption": "My Plugin",
        "children": [
            {
                "caption": "Insert Timestamp",
                "command": "insert_timestamp"
            },
            {
                "caption": "Uppercase Selection",
                "command": "upper_case_selection"
            }
        ]
    }
]

Main Menu Entry

Create a file named Main.sublime-menu to add items to the top menu bar:

[
    {
        "caption": "My Plugin",
        "mnemonic": "M",
        "id": "my-plugin",
        "children": [
            {
                "caption": "Insert Timestamp",
                "command": "insert_timestamp"
            },
            { "caption": "-" },
            {
                "caption": "Preferences",
                "command": "edit_settings",
                "args": {
                    "base_file": "${packages}/MyPlugin/MyPlugin.sublime-settings",
                    "default": "{\n\t// MyPlugin settings\n}\n"
                }
            }
        ]
    }
]

Asynchronous Operations and Threading

Sublime Text runs plugins on the main thread by default. Long-running operations will freeze the UI, so you must use asynchronous execution for tasks like network requests or heavy computation.

Using set_timeout_async

import sublime
import sublime_plugin
import urllib.request
import json


class FetchApiDataCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        sublime.status_message("Fetching data...")
        sublime.set_timeout_async(self.fetch_data, 0)

    def fetch_data(self):
        try:
            url = "https://api.github.com/repos/sublimehq/sublime_text"
            with urllib.request.urlopen(url) as response:
                data = json.loads(response.read().decode())
                stars = data.get("stargazers_count", "unknown")
                # Update UI on the main thread
                sublime.set_timeout(
                    lambda: self.insert_result(stars), 0
                )
        except Exception as e:
            sublime.set_timeout(
                lambda: sublime.error_message(f"API Error: {e}"), 0
            )

    def insert_result(self, stars):
        edit = self.view.begin_edit()
        for region in self.view.sel():
            self.view.insert(edit, region.begin(), f"Stars: {stars}")
        self.view.end_edit(edit)

Using Threads Directly

import threading


class BackgroundWorker:
    def __init__(self, view):
        self.view = view

    def start(self):
        thread = threading.Thread(target=self.work, daemon=True)
        thread.start()

    def work(self):
        # Perform heavy work here
        result = self.expensive_computation()
        # Schedule UI update on main thread
        sublime.set_timeout(lambda: self.update_ui(result), 0)

    def expensive_computation(self):
        import time
        time.sleep(3)
        return "Done!"

    def update_ui(self, result):
        sublime.status_message(result)

Always remember: any operation that touches the Sublime Text API (views, windows, settings) must run on the main thread. Use sublime.set_timeout to schedule such operations from background threads.

Building a Complete Plugin Example

Let us build a more complete plugin called WordCounter that counts words in the current file and displays the result in the status bar. This example ties together commands, event listeners, and settings.

Directory Structure

WordCounter/
โ”œโ”€โ”€ word_counter.py
โ”œโ”€โ”€ WordCounter.sublime-settings
โ”œโ”€โ”€ Default.sublime-commands
โ”œโ”€โ”€ Context.sublime-menu
โ””โ”€โ”€ messages/
    โ””โ”€โ”€ 1.0.0.txt

word_counter.py

import sublime
import sublime_plugin


def count_words(view):
    """Count words in the given view."""
    content = view.substr(sublime.Region(0, view.size()))
    words = content.split()
    return len(words)


def count_chars(view):
    """Count characters in the given view."""
    return view.size()


def count_lines(view):
    """Count lines in the given view."""
    content = view.substr(sublime.Region(0, view.size()))
    return content.count("\n") + 1


def update_status(view):
    """Update the status bar with word count."""
    settings = sublime.load_settings("WordCounter.sublime-settings")
    if not settings.get("enabled", True):
        view.erase_status("word_counter")
        return

    words = count_words(view)
    chars = count_chars(view)
    lines = count_lines(view)

    fmt = settings.get("status_format", "Words: {words} | Chars: {chars} | Lines: {lines}")
    message = fmt.format(words=words, chars=chars, lines=lines)
    view.set_status("word_counter", message)


class WordCounterListener(sublime_plugin.EventListener):
    def on_modified(self, view):
        update_status(view)

    def on_load(self, view):
        update_status(view)

    def on_activated(self, view):
        update_status(view)

    def on_selection_modified(self, view):
        settings = sublime.load_settings("WordCounter.sublime-settings")
        if settings.get("show_selection_count", False):
            sel = view.sel()[0]
            if not sel.empty():
                selected_text = view.substr(sel)
                word_count = len(selected_text.split())
                view.set_status("word_counter_sel", f"Selected: {word_count} words")
            else:
                view.erase_status("word_counter_sel")


class WordCountCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        words = count_words(self.view)
        chars = count_chars(self.view)
        lines = count_lines(self.view)

        message = f"Word Count: {words} words, {chars} characters, {lines} lines"
        sublime.message_dialog(message)


class ToggleWordCounterCommand(sublime_plugin.ApplicationCommand):
    def run(self):
        settings = sublime.load_settings("WordCounter.sublime-settings")
        current = settings.get("enabled", True)
        settings.set("enabled", not current)
        sublime.save_settings("WordCounter.sublime-settings")

        status = "enabled" if not current else "disabled"
        sublime.status_message(f"WordCounter {status}")

        # Update all views
        for window in sublime.windows():
            for view in window.views():
                if not current:
                    update_status(view)
                else:
                    view.erase_status("word_counter")

WordCounter.sublime-settings

{
    "enabled": true,
    "show_selection_count": false,
    "status_format": "Words: {words} | Chars: {chars} | Lines: {lines}"
}

Default.sublime-commands

[
    {
        "caption": "Word Counter: Show Count",
        "command": "word_count"
    },
    {
        "caption": "Word Counter: Toggle",
        "command": "toggle_word_counter"
    }
]

Packaging and Distribution

Once your plugin is complete, you will want to share it with the community. The standard distribution channel is Package Control, the de facto package manager for Sublime Text.

Creating a Package Metadata File

Create a package-metadata.json file in your plugin folder:

{
    "name": "WordCounter",
    "version": "1.0.0",
    "description": "Real-time word, character, and line counting in the status bar.",
    "author": "Your Name",
    "homepage": "https://github.com/yourusername/WordCounter",
    "issues": "https://github.com/yourusername/WordCounter/issues"
}

Adding a README

Include a README.md file with installation instructions, usage examples, configuration options, and screenshots. A well-documented README significantly increases adoption.

Submitting to Package Control

To get your plugin listed in Package Control:

The entry in the repository file looks like this:

{
    "name": "WordCounter",
    "details": "https://github.com/yourusername/WordCounter",
    "releases": [
        {
            "sublime_text": "*",
            "details": "https://github.com/yourusername/WordCounter/tags"
        }
    ],
    "labels": ["text", "statistics", "productivity"]
}

Adding Release Messages

You can show users a message when they install or update your plugin. Create a messages folder with files named after version numbers (e.g., 1.0.0.txt) and add an entry in messages.json:

{
    "install": "messages/install.txt",
    "1.0.0": "messages/1.0.0.txt",
    "1.1.0": "messages/1.1.0.txt"
}

Best Practices

1. Name Your Commands Carefully

Command names are global. Prefix them with your plugin name to avoid collisions with other plugins. For example, use word_counter_show instead of show.

2. Keep the UI Responsive

Never perform long-running operations on the main thread. Use sublime.set_timeout_async or Python threads for network requests, file I/O, and heavy computation. Always schedule UI updates back on the main thread with sublime.set_timeout.

3. Respect User Settings

Always provide sensible defaults and allow users to override behavior through settings. Check settings at runtime rather than caching them at module load time, unless you also listen for changes.

4. Scope Your Event Listeners

Event listeners fire for every view. Always check the file type or scope before performing work to avoid unnecessary processing:

def on_modified(self, view):
    if not view.match_selector(0, "source.python"):
        return
    # Proceed with Python-specific logic

5. Handle Errors Gracefully

Wrap risky operations in try/except blocks and provide meaningful feedback to the user. Unhandled exceptions in plugins can destabilize the editor.

try:
    result = perform_risky_operation()
except Exception as e:
    sublime.error_message(f"MyPlugin encountered an error:\n\n{e}")
    return

6. Use Semantic Versioning

Follow semantic versioning (MAJOR.MINOR.PATCH) for your releases. This helps users understand the impact of updates and allows Package Control to handle upgrades correctly.

7. Write Clean, Documented Code

Since plugins are open source, write code that others can read and contribute to. Add docstrings, comments, and type hints where appropriate. Organize your code into logical modules if the plugin grows large.

8. Test Across Platforms

Sublime Text runs on Windows, macOS, and Linux. Test your plugin on all three platforms, especially if you use platform-specific features like file paths or external commands.

9. Avoid Global State

Minimize the use of module-level variables. Global state can lead to subtle bugs, especially when multiple windows or views are involved. Use settings or per-view storage instead.

10. Clean Up After Yourself

If your plugin registers callbacks, timers, or creates temporary files, make sure to clean them up when the plugin is unloaded or when views are closed. Use plugin_unloaded for cleanup:

def plugin_unloaded():
    # Clean up resources
    settings = sublime.load_settings("MyPlugin.sublime-settings")
    settings.clear_on_change("my_plugin_reload")
    sublime.status_message("MyPlugin unloaded")

Debugging Techniques

Using the Console

The Sublime Text console (accessible via Ctrl+`) is your primary debugging tool. Use print() statements liberally during development. You can also interact with the API directly in the console:

# Inspect the active view
view = sublime.active_window().active_view()
print(view.file_name())
print(view.settings().get("syntax"))

# Run a command
view.run_command("insert_timestamp")

# Inspect all loaded packages
print(sublime.packages_path())

Logging to a File

For complex plugins, consider logging to a file for persistent debugging:

import logging
import os

log_path = os.path.join(sublime.packages_path(), "MyPlugin", "debug.log")
logging.basicConfig(
    filename=log_path,
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("MyPlugin")

# Usage
logger.info("Plugin started")
logger.debug(f"View size: {view.size()}")

Handling Reloads During Development

Sublime Text automatically reloads Python files when you save them, but sometimes you need a full reload. You can use the PackageDev package or manually reload in the console:

import importlib
import MyPlugin.word_counter
importlib.reload(MyPlugin.word_counter)

Advanced Topics

Phantom and HTML Popups

Sublime Text 4 supports phantoms (inline HTML content) and popups, which allow you to create rich UI elements within the editor:

class ShowPhantomCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        view = self.view
        sel = view.sel()[0]
        word = view.substr(view.word(sel.begin()))

        content = f'''
        
            
            
Word: {word}
Length: {len(word)} characters

Working with Multiple Cursors

Sublime Text is famous for its multiple cursor support. Your plugins should handle multiple selections gracefully:

class WrapSelectionCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        wrapper = self.view.settings().get("wrap_character", '"')
        for region in self.view.sel():
            if not region.empty():
                text = self.view.substr(region)
                wrapped = f"{wrapper}{text}{wrapper}"
                self.view.replace(edit, region, wrapped)

Creating Build Systems

You can bundle build systems with your plugin. Create a .sublime-build file:

{
    "selector": "source.mylang",
    "cmd": ["mylang", "$file"],
    "file_regex": "^(.+):(\\d+):(\\d+):\\s+(.+)$",
    "working_dir": "$file_path",
    "variants": [
        {
            "name": "Run",
            "cmd": ["mylang", "--run", "$file"]
        },
        {
            "name": "Check Syntax",
            "cmd": ["mylang", "--check", "$file"]
        }
    ]
}

Conclusion

Sublime Text plugins offer a powerful way to extend and personalize one of the fastest and most beloved code editors available. By leveraging Python and the comprehensive Sublime Text API, you can build everything from simple text manipulation commands to full-featured language support tools. The key to successful plugin development lies in understanding the command system, mastering the API objects (View, Window, and the sublime module), respecting the main-thread constraint for UI operations, and following community best practices for

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles