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.