16. Input Handlers

Input Handlers augment normal Commands by enabling those commands to gather input directly from the user instead of from a fixed input list, e.g. from a Key Binding.

Each Input Handler used by a Command will “fill in” exactly one argument in the args dictionary that is eventually passed to the Command. That “passing” does not happen until the user has successfully gone through all the provided Input Handlers in sequence without aborting with the [Esc] key or other means.

Input Handlers are created as sub-classes of several classes found in sublime_plugin.py. Their names end with “...InputHandler”, and they all derive from the CommandInputHandler class, where most of the common methods (which you can optionally override/redefine) are found. In the order found in sublime_plugin.py:

  • BackInputHandler

  • TextInputHandler

  • ListInputHandler

The Input Handler object’s name(self) method provides the name of the argument that gets created for the args dictionary. Its default implementation translates the name of the class you create into “snake case” and then chops off the trailing “_input_handler” if it is present. For example, the name “PositionInputHandler” will create a lower-case argument named position in the args dictionary that eventually gets passed to the Command. An underscore (‘_’) is inserted into the name each time the class name transitions from a lower-case to an upper-case letter. So the class name “AlphaBRAvoCharlieInputHandler” would be translated to the name “alpha_bRAvo_charlie”.

Important!

Input handlers only work for commands in the Command Palette. Therefore, to use them with any Command you create, you must also create that Command in a any_name.sublime-commands file to get the command into the Command Palette, even though your Command may be invoked from a Key Binding or Menu-Item selection as well.

16.1. Flow of Execution

When the command is run, whether it be from:

  • a Key Binding being triggered,

  • a Menu-Item selection by the user,

  • the Command Palette, or

  • programmatically (e.g. from a Plugin) by something like this:

    self.view.run_command('command_name', args)
    

    where args is a dictionary that would normally provide the named positional arguments to the Command’s run(self, edit, ...) method.

Sublime Text attempts to run the command with the args dictionary provided, and if it then triggers an exception based on unfilled positional arguments, it calls that command’s input(self, args) input method to ask the Command, “Do you know how I can get the missing arguments?”

Important!

You trigger the input sequence by providing an incomplete set of arguments in the Command’s `args` dictionary. Example:

class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit, message, position):
        # do something here

See example below.

Once the Command’s input(self, args) method has been called, its job is to detect what arguments are missing and return to the caller an instance of the “...InputHandler” class you created in order to gather that exact argument for your Command. Because the Command can be “called” from any number of sources with any combination of missing arguments (including by end users who use your package), the input(self, args) method should check for all possible missing arguments, in the same sequence you want the user prompted for them.

At this point Sublime Text makes the Input Overlay Panel visible. If the Command has an input_description(method) method, it will be called to get the string to use as the prompt to the left of the cursor when the input sequence is in progress.

Each time the user submits his input (by hitting [Enter]), the Sublime Text tries again to call the command. If it still gets an exception based on unfilled positional arguments, it attempts to call the next_input(self, args) method of the most-recently-used Input Handler object. That method can return one of the following:

  • an instance of the Input-Handler class you created for your Command, that will gather the next input argument needed in the args dictionary for your Command;

  • an instance of sublime_plugin.BackInputHandler() to cause the input sequence to “go back” to the previous state (e.g. previous input handler); or

  • None which signals the caller that the last argument entry for the args dictionary has been populated and to go ahead and call the Command and pass the args dictionary as it is. Note: returning None can also be done by letting the method run past its end with no return statement.

Note

The user goes forward in the input sequence by hitting [Enter] and can choose to go backward in the input sequence by hitting [Backspace].

16.1.1. TextInputHandler

Sub-class from sublime_plugin.TextInputHandler to create an Input Handler that will gather input from the user in a free-form typing format, on one line of text. These can also have an output-display area. A default value can be provided if desired, and there is a convenient way you can use to validate input. You can find an example of this in arithmetic.py that ships with Sublime Text.

Common methods to define for text input include:

  • initial_text(self) optionally returns text that can be an initial value populating the text field when the user is prompted. If it returns None then the TextBox shown to the user will be empty.

  • next_input(self, args) is called by the Command Palette to get the next Input Handler if there are still more arguments to fill in the args dictionary before the Command is eventually called. See above to see this method’s responsibility.

16.1.2. ListInputHandlers

Sub-class from sublime_plugin.ListInputHandlers to create an Input Handler that functions similar to a drop-down list, allowing the user to select from a number of options. “Fuzzy searching” in the list is provided automatically. A default selection can be provided if desired.

Common methods to define for list input include:

  • list_items(self) which should return a list of tuples, one for each item the user can select from, each containing:

    • a string (shown to user to select), and

    • the value that string represents, that will become the value of that args dictionary entry that this Input Handler provides.

  • next_input(self, args) is called by the Command Palette to get the next Input Handler if there are more arguments to fill in the args dictionary before the Command is eventually called. This is exactly as described above for the TextInputHandler sub-class.

16.1.3. Example

To see this running in a simple, running example, use the code below. Also add command odat_nurd_example0910 to a test Key Binding with different arguments missing to see how Sublime Text uses these things. Comments are added to overridden methods below to explain what they are for and when to override/redefine them.

Put this in any-named .sublime-commands file in the root of your User Package, e.g. odatnurd-p101-09-example.sublime-commands.

[
    {
        "caption": "OdatNurd Example 09+10 Command",
        "command": "odat_nurd_example0910",
        "args": {
            // "message": "My message.",
            // "position": 0
            // "after_text": "My additional statement."
        }
    }
]

Put the following in any-named .py file in the root of your User Package, e.g. odatnurd-p101-09+10-example.py.

import sublime
import sublime_plugin
import pprint


class MessageInputHandler(sublime_plugin.TextInputHandler):
    def name(self):
        """
        Name of argument this Input Handler fills.

        The default definition for this method converts the name of the class
        (e.g. "MessageInputHandler") into just "message". When overridden,
        this Input Handler can provide a different name if desired.
        """
        return 'message'

    def initial_text(self):
        """
        Initial text shown in the text entry box. Empty by default.
        """
        return "Hello, world!"

    def initial_selection(self) -> [(int, int)]:
        """
        A list of 2-element Region tuples, defining the initially
        selected parts of the initial text.
        """
        return []

    def placeholder(self):
        """
        This placeholder text is shown in the background of the text entry
        box (grayed out) whenever it is empty.  Empty by default.
        """
        return 'Enter message to insert (not "Hello")'

    def description(self, text):
        """
        The text to show in the *Command Palette* when this input handler is not
        at the top of the input handler stack.  Defaults to the text the user
        entered.
        """
        return '<msg>'

    def preview(self, text):
        """
        Called whenever the user changes the text in the entry box. The returned
        value (either plain text or HTML) will be shown in the preview area of
        the *Command Palette*.
        """
        # return f'Inserting: [{text}]'
        return sublime.Html(f'<strong>Inserting:</strong> <em>{text}</em>')

    def validate(self, text, event):
        """
        Called when user hits [Enter] to submit input.  If this method returns
        `False`, nothing happens.  If it returns `True`, the input sequence proceeds.
        Default implementation returns `True`.

        :param event:  Gets passed when `want_event` returns `True`.  User hitting
                       plain [enter] results in `event` containing
                         `{'modifier_keys': {}}`.
                       [ctrl+enter] results in `event` containing
                         `{'modifier_keys': {'ctrl': True, 'primary': True}}`.
                       [shift+ctrl+alt] results in `event` containing
                         `{'modifier_keys': {'alt': True, 'ctrl': True, 'primary': True, 'shift': True}}`

                       "primary" means the View is the first View attached to
                       its buffer.  "primary": False would happen in a View that
                       is "cloned".
        """
        print('validate() running...  Event object shown below.')
        pprint.pp(event, indent=4)
        result = True

        if text == 'Hello':
            result = False

        return result

    def cancel(self):
        """
        Called as an "event hook" if/when user cancels input sequence with [Esc]
        or [Backspace] to go back.
        """
        print('User cancelled at MessageInputHandler.')

    def confirm(self, text, event):
        """
        Called when the input is accepted, after the user has pressed enter and
        the text has been validated.

        :param event:  Gets passed when `want_event` returns `True`.  User hitting
                       plain [enter] results in `event` containing
                         `{'modifier_keys': {}}`.
                       [ctrl+enter] results in `event` containing
                         `{'modifier_keys': {'ctrl': True, 'primary': True}}`.
                       [shift+ctrl+alt] results in `event` containing
                         `{'modifier_keys': {'alt': True, 'ctrl': True, 'primary': True, 'shift': True}}`
        """
        print(f'Message.confirm():  Got [{text}].  Event object below.')
        pprint.pp(event, indent=4)

    def want_event(self) -> bool:
        """
        Whether the `validate()` and `confirm()` methods should received a
        second `Event`-type parameter.  Returns `False` by default.
        """
        return True

    def next_input(self, args):
        if 'position' not in args:
            return PositionInputHandler()


class PositionInputHandler(sublime_plugin.ListInputHandler):
    def list_items(self):
        # Option 1:  a list of strings to choose from; good for text options.
        # return ['0', '50', '-1', '-100']
        #
        # Option 2:  a list of tuples, the user sees the first element, the
        #            second element is the value.
        # return [
        #     ('Top of the file', 0),
        #     ('Middle of the file', 50),
        #     ('Bottom of the file', -1),
        #     ('Cancel', -100)
        # ]

        # Option 3:  a tuple that contains a list as in option 2, and also
        #            the index of the item that should be selected by default.
        return (
                [
                    ('Top of the file', 0),
                    ('Middle of the file', 50),
                    ('Bottom of the file', -1),
                    ('Cancel', -100)
                ],
                2
            )

    def description(self, value, text: str) -> str:
        """
        The text to show in the *Command Palette* when this input handler is not
        at the top of the input handler stack. Defaults to the text of the list
        item the user selected.
        """
        return f'Value [{value}]'

    def preview(self, value):
        """
        Called whenever the user changes the text in the entry box. The returned
        value (either plain text or HTML) will be shown in the preview area of
        the *Command Palette*.
        """
        #return f'Inserting at position: [{value}]'
        return sublime.Html(f'<strong>Inserting at position:</strong> <em>{value}</em>')

    def initial_text(self):
        """
        Initial text as if user typed this in.  Will cause "fuzzy filtering"
        to be applied to the list.
        """
        return ''

    def cancel(self):
        """
        Called as an "event hook" if/when user cancels input sequence with [Esc]
        or [Backspace] to go back.
        """
        print('User cancelled at PositionInputHandler.')

    def confirm(self, value):
        """
        Called when the input is accepted, after the user has pressed enter and
        the text has been validated.

        :param event:  Gets passed when `want_event` returns `True`.  User hitting
                       plain [enter] results in `event` containing
                         `{'modifier_keys': {}}`.
                       [ctrl+enter] results in `event` containing
                         `{'modifier_keys': {'ctrl': True, 'primary': True}}`.
                       [shift+ctrl+alt] results in `event` containing
                         `{'modifier_keys': {'alt': True, 'ctrl': True, 'primary': True, 'shift': True}}`

                       "primary" means the View is the first View attached to
                       its buffer.  "primary": False would happen in a View that
                       is "cloned".
        """
        print(f'Position.confirm():  Got [{value}].  Event object below.')
        self.selected_value = value

    def next_input(self, args):
        if self.selected_value == -100:
            return sublime_plugin.BackInputHandler()
        elif 'after_text' not in args:
            return AfterTextInputHandler()


class AfterTextInputHandler(sublime_plugin.TextInputHandler):
    def placeholder(self):
        """
        This placeholder text is shown in the background of the text entry
        box (grayed out) whenever it is empty.  Empty by default.
        """
        return 'Any additional text you want to add.'

    def confirm(self, text, event):
        """
        Called when the input is accepted, after the user has pressed enter and
        the text has been validated.

        :param event:  Gets passed when `want_event` returns `True`.  User hitting
                       plain [enter] results in `event` containing
                         `{'modifier_keys': {}}`.
                       [ctrl+enter] results in `event` containing
                         `{'modifier_keys': {'ctrl': True, 'primary': True}}`.
                       [shift+ctrl+alt] results in `event` containing
                         `{'modifier_keys': {'alt': True, 'ctrl': True, 'primary': True, 'shift': True}}`

                       "primary" means the View is the first View attached to
                       its buffer.  "primary": False would happen in a View that
                       is "cloned".
        """
        print(f'AfterText.confirm():  Got [{text}].  Event object below.')
        pprint.pp(event, indent=4)

    def want_event(self) -> bool:
        """
        Whether the `validate()` and `confirm()` methods should received a
        second `Event`-type parameter.  Returns `False` by default.
        """
        return True

    def cancel(self):
        """
        Called as an "event hook" if/when user cancels input sequence with [Esc]
        or [Backspace] to go back.
        """
        print('User cancelled at AfterTextInputHandler.')


class OdatNurdExample0910Command(sublime_plugin.TextCommand):
    def run(self, edit, message, position, after_text):
        if position < 0:
            position = self.view.size()

        self.view.insert(edit, position, message + after_text)

    def input(self, args):
        # Note: the names of these keys being tested for in the `args`
        # dictionary is created by the "stem" of the name of the
        # Input-Handler class.  In this case, "stem" means the part
        # of the class name before "...InputHandler" at the end.
        if 'message' not in args:
            return MessageInputHandler()
        elif 'position' not in args:
            return PositionInputHandler()
        elif 'after_text' not in args:
            return AfterTextInputHandler()

    def input_description(self):
        return 'Insert'