Sublime Forum

How to multiple TextInputHandlers work?

#1

I can make a TextCommand have an input method that returns a TextInputHandler, and that works fine. The question is, what if I need more than once piece if input?

It looks like what I need is the next_input method of TextInputHandler, since it says it can return the next input handler to use. It can return None to say there’s no more input to collect or pass back a sentinel value to say to pop it off the stack. But what stack? I don’t see anything that creates a stack of input handlers.

I’m sure this can be made to work by having the TextCommand’s input() method construct an TextInputHandler with a list of missing arguments and then have the TextInputHandler()'s next_input() keep instantiating new handlers until the list is empty, but it seems kind of silly to have the TextInputHandlers manage lists of missing arguments from TextCommand. If there’s a way the TextCommand could create a stack of InputHandlers, that seems like a much better way to do it.

I haven’t found any examples of next_input(), presumably because the feature is so new. Does anyone know how this is intended to be used?

0 Likes

#2

The general gist of how this mechanism works is that while executing a command, Sublime catches TypeError exceptions and introspects the error message to see if it’s mentioning a missing argument. If that’s the case it kicks off asking for user input for the arguments in the command palette, and if not it re-raises the exception so that the command fails as it normally would. The ordering of input prompts is governed by input() and next_input().

The input() method is for indicating what input handler the user should be prompted with first and you can consider the return value of this being either an indication that there is no input handler that applies or a push onto a stack of input handlers, depending on the circumstances.

When the next_input() of an input handler returns None, the input is considered finished and Sublime tries to execute the command with the arguments that it has so far. If the return value is instead another input handler, then that is a push of that input handler onto the stack, and you get prompted to input the next item. If instead you return the special sentinel value, the current input handler is popped off and input returns to the previous input handler (i.e. the one that returned this input handler in it’s next_input() method.

By way of an illustrative example of this, lets assume a contrived command like the following:

class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit, first, second):
        sublime.status_message("%s,%s" % (first, second))

Added to the command palette with an entry like this (i.e. no arguments):

[
    { "caption": "Example", "command": "example" }
]

Since the command takes two required arguments and neither are specified in the command palette entry, if you try to execute the command it fails due to missing arguments as one might expect. So we include a couple of InputHandler classes, one for each of the two arguments that the command takes (and the name of the argument is based on the class name unless you tell Sublime differently, etc).

Minimally, that might look like the following, with just enough implementation so we can see what argument we’re being prompted to enter:

class FirstInputHandler(sublime_plugin.TextInputHandler):
    def placeholder(self):
        return "First Argument"


class SecondInputHandler(sublime_plugin.TextInputHandler):
    def placeholder(self):
        return "Second Argument"


class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit, first, second):
        sublime.status_message("%s,%s" % (first, second))

    def input(self, args):
        print("Input in Example: ", args)

        if not args.get("first"):
            return FirstInputHandler()

        if not args.get("second"):
            return SecondInputHandler()

Now when you execute the command from the command palette, you get prompted first for the value of the first argument, then for the second, and then you see the results in the status bar when you confirm the second input.

At any point, if you press Backspace enough times, you will “backspace” over the name of the command and return back to the command palette command list.

When the command executes, if you look in the console, you can see that the print line in input() is triggering more than once here; first it gets invoked with no arguments at all, then it gets called again and now it has the value of the first argument that you provided in the command palette.

That’s the input handler mechanism at work; initially it tried to run the command but had no arguments, so input() was called with no arguments and the result was the input handler for first prompting you. That input handler has no next_input(), so input is considered to be complete and Sublime tries to run the command again. This is still a problem because second is missing, so you get prompted for that argument instead. It also has no next_input(), so when the input is complete it tries to run the command again, and this time it works.

Next, we modify the SecondInputHandler like so:

class SecondInputHandler(sublime_plugin.TextInputHandler):
    def placeholder(self):
        return "Second Argument"

    def confirm(self, text):
        self.text = text

    def next_input(self, args):
        if self.text == "back":
            return sublime_plugin.BackInputHandler()

In this one, we handle confirm() to store the text that the user entered, and in next_input() we check to see if it’s the text back and if so we return the value that tells Sublime to pop this handler off of the “input stack”.

With this in place, run the command again and enter a value for the first argument, and then for the second argument input the text back. When you enter that argument, the command palette jumps back to the list of commands as if you hadn’t executed the command yet.

This is the pop happening; from the perspective of Sublime you tried to run the command with a value for first but not for second, and then you told it to go back from whence it came, which is the command palette.

Next we modify FirstInputHandler() like so:

class FirstInputHandler(sublime_plugin.TextInputHandler):
    def placeholder(self):
        return "First Argument"

    def next_input(self, args):
        if "second" not in args:
            return SecondInputHandler()

This tells the FirstInputHandler that the input that comes after it is the input for the second argument, if that argument is not already present in the list of arguments to the command. Now we take the same steps as above; enter a value for the first argument and then the text back for the second argument. This time something different happens.

First, when you enter the value for the first argument, as you’re being prompted for the second argument you can see the value of the first one, showing you what was entered (this is a visualization of the “stack”):

Secondly, when you enter the text back for the value of the second argument, instead of going back to the list of commands in the command palette, you get sent back to the input handler for the first argument instead. You can also use Backspace to “erase” the first value, which immediately takes you back to entering the value for the first argument.

This particular example presumes the case that you want to allow the command to be executed with potentially only partial arguments and prompt the user for any that are missing; in that case at every step along the way you need to check and see what the “next” input would be.

In a more practical use, you might not bother with all of the conditionals in here; you could for example have input() always return FirstInputHandler() and have FirstInputHandler.next_input() always return SecondInputHandler(). In that case you’re presuming that either the command palette executes the command with every argument or that you want the user to be forced to specify every one.

You could also have a helper class that knows the input order as you suggest and pass it around between input handlers via their __init__ methods so that the ordering logic is in a centralized place. I’ve done that previously although I can’t seem to find the code for it at the moment.

2 Likes

#3

Thanks for the information! This makes a lot of sense. The documentation is quite terse.

It sounds like from a practical matter, that the reason to use next_input(), as opposed to using input() multiple times, is to get nicer behavior for backspace.

0 Likes

#4

Yeah I would imagine from a UX perspective you want to use next_input() to chain the handlers together when you have more than one argument you’re asking for because that allows the person running the command to better see the command that they’re building up (a nice example of that is a git push operation in Merge, if you use that).

You can also use the description() method in the handlers to control what appears for each item in the stack (the default is the text entered as visualized above).

0 Likes

#5

I apologize for reviving this old thread, but at the same time I think it doesn’t hurt to bump such an excellent tutorial from @OdatNurd.

I wonder whether it is possible to start with one or multiple InputHandlers already pushed onto the stack, when a command is executed. I’d like to provide a default value (or read it from a settings file) for one of the parameters, but still allow users to press backspace to discard it and select another value, if desired.

To clarify what I mean, consider the push command in Sublime Merge; if you select “Push…” from the command palette, it already has the current branch and remote name preselected and displays those values in the input field, but it is still possible to adjust them with backspace.

merge

Is there a way to achieve the same from a plugin in Sublime Text? Unfortunately my attempts were not successful.

0 Likes

#6

As far as I’m aware, that’s not possible unless there’s some secret internals in the API (such as extra arguments to the show_overlay command) that can front load such things.

I suspect the reason this works as it does in Merge is because all of the commands there are implemented in the core rather than as plugins (as most commands are in Text) and as such they have additional access to the internals that can’t currently be matched.

0 Likes