-
-
Save OdatNurd/fd6322a665c1730c7e16930b3a84999a to your computer and use it in GitHub Desktop.
3.8 |
import sublime | |
import sublime_plugin | |
from sublime import QuickPanelItem | |
import inspect | |
import re | |
import sys | |
from sublime_plugin import application_command_classes | |
from sublime_plugin import window_command_classes | |
from sublime_plugin import text_command_classes | |
## ---------------------------------------------------------------------------- | |
KIND_APPLICATION = (sublime.KIND_ID_FUNCTION, "A", "Application Command") | |
KIND_WINDOW = (sublime.KIND_ID_FUNCTION, "W", "Window Command") | |
KIND_TEXT = (sublime.KIND_ID_FUNCTION, "T", "Text Command") | |
cmd_types = { | |
"app": { | |
"name": "ApplicationCommand", | |
"commands": application_command_classes, | |
"kind": KIND_APPLICATION | |
}, | |
"wnd": { | |
"name": "WindowCommand", | |
"commands": window_command_classes, | |
"kind": KIND_WINDOW | |
}, | |
"txt": { | |
"name": "TextCommand", | |
"commands": text_command_classes, | |
"kind": KIND_TEXT | |
} | |
} | |
## ---------------------------------------------------------------------------- | |
def _navigate_to(view, symbol): | |
""" | |
Navigate to the symbol in the given view. | |
""" | |
view.window().run_command("goto_definition", {"symbol": symbol}) | |
## ---------------------------------------------------------------------------- | |
class BrowseCommandsCommand(sublime_plugin.ApplicationCommand): | |
""" | |
Open a quick panel with a list of all commands known to the Sublime plugin | |
host for the user to choose from. Picking a command opens the plugin file | |
containing the command and navigates to the location where the command is | |
defined. | |
This command is exposed to both plugin hosts (by way of being in two | |
packages) with the Python 3.3 plugin host triggering the command from the | |
Python 3.8 host, so that we can gather all available commands. | |
""" | |
arg_re = re.compile(r"^\(self(?:, )?(?:edit, |edit)?(.*)\)$") | |
def legacy(self): | |
return sys.version_info < (3, 8, 0) | |
def name(self): | |
return "browse_commands_33" if self.legacy() else "browse_commands" | |
def run(self, cmd_dict=None): | |
cmd_dict = cmd_dict or {} | |
for cmd_type, cmd_info in cmd_types.items(): | |
self.get_commands(cmd_type, cmd_info["commands"], cmd_dict) | |
if self.legacy(): | |
return sublime.run_command('browse_commands', {"cmd_dict": cmd_dict}) | |
items = [] | |
for command,details in cmd_dict.items(): | |
items.append(QuickPanelItem(details["name"], | |
"<i>%s</i>" % details["args"], | |
"%s.%s" % (details["pkg"] , details["mod"]), | |
cmd_types[details["type"]]["kind"])) | |
items.sort(key=lambda o: o.trigger) | |
sublime.active_window().show_quick_panel(items, | |
lambda i: self.pick(i, items, cmd_dict)) | |
def pick(self, idx, items, cmd_dict): | |
if idx == -1: | |
return | |
cmd = cmd_dict[items[idx].trigger] | |
module = cmd["mod"].replace('.', '/') | |
res = '${packages}/%s/%s.py' % (cmd["pkg"], module) | |
sublime.active_window().run_command('open_file', {'file': res}) | |
view = sublime.active_window().active_view() | |
if view.is_loading(): | |
view.settings().set("_jump_to_class", cmd["class"]) | |
else: | |
_navigate_to(view, cmd["class"]) | |
def get_commands(self, cmd_type, commands, cmd_dict_out): | |
""" | |
Given a list of commands of a particular type, decode each command in | |
the list into a dictionary that describes it and store it into the | |
output dictionary keyed by the package that defined it. | |
The output dictionary gains keys for each package, where the values are | |
dictionaries which contain keys that describe the commands of each of | |
the supported typed. | |
""" | |
for command in commands: | |
decoded = self.decode_cmd(command, cmd_type) | |
cmd_dict_out[decoded["name"]] = decoded | |
def decode_cmd(self, command, cmd_type): | |
""" | |
Given a class that implements a command of the provided type, return | |
back a dictionary that contains the properties of the command for later | |
display. | |
""" | |
return { | |
"type": cmd_type, | |
"pkg": command.__module__.split(".")[0], | |
"mod": ".".join(command.__module__.split(".")[1:]), | |
"name": self.get_name(command), | |
"args": self.get_args(command), | |
"class": command.__name__ | |
} | |
def get_args(self, cmd_class): | |
""" | |
Return a string that represents the arguments to the run method of the | |
Sublime command class provided, edited to remove the internal python | |
arguments that are not needed to invoke the command from Sublime. | |
""" | |
args = str(inspect.signature(cmd_class.run)) | |
return self.arg_re.sub(r"{ \1 }", args) | |
def get_name(self, cmd_class): | |
""" | |
Return the internal Sublime command name as Sublime would infer it from | |
the name of the implementing class. This is taken from the name() | |
method of the underlying Command class in sublime_plugin.py. | |
""" | |
clsname = cmd_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 | |
## ---------------------------------------------------------------------------- | |
class CommandJumpListener(sublime_plugin.ViewEventListener): | |
@classmethod | |
def is_applicable(cls, settings): | |
return settings.has("_jump_to_class") | |
def on_load(self): | |
symbol = self.view.settings().get("_jump_to_class") | |
self.view.settings().erase("_jump_to_class") | |
sublime.set_timeout(lambda: _navigate_to(self.view, symbol), 0) |
[ | |
{ "caption": "Browse Command List", "command": "browse_commands_33" }, | |
] |
The development of this plugin was done in Live Stream #76, for those interested in how it works.
Did you consider adding it to PackageDev ?
I did not, but to work fully it requires the package be installed once for each plugin host so there needs to be some extra stuff happening there to get things going. I'm not sure what the best method to do that in a generic sense is, although AutomaticPackageReloader does something similar to support reloading in both plugin hosts.
I don't know, but I see it as potentially helpful to be included in PD, maybe ask on Discord ?
I seem to find a weird problem. Theoretically, if I want it to work for finding both py33/38 commands, I should have two copy of this plugin in different directories. One of them is executed in the py33 plugin_host
and the other one is in py38, which needs a .python-version
with 3.8
in it. So that console shows the following when ST starts up.
reloading plugin my_plugin_38.browse_all_commands
reloading python 3.3 plugin my_plugin_33.browse_all_commands
After doing that, I will need the following menu for each of them:
[
{
"caption": "Browse Command List (py33)",
"command": "browse_commands_33",
},
{
"caption": "Browse Command List (py38)",
"command": "browse_commands",
},
]
The above setup works well. Both commands are doing their jobs indeed.
The weird thing is that, say, I change something in the py38 file, such as the ApplicationCommand
kind
icon from A
to X
, it affects both browse_commands_33
and browse_commands
commands from the command palette. Changing something in the py33 file seems useless.
If you want them to both be separate commands, you need to adjust these lines: https://gist.github.com/OdatNurd/fd6322a665c1730c7e16930b3a84999a#file-browse-py-L79-L80
In the 3.3 host, the command gathers a list of commands and then executes the version of the command in the 3.8 host with them so that it can produce one unified list with all commands regardless of what host they're in. So as presented above modifying how things display in the 3.3 version has no effect because that part of the command never gets executed.
Ah, it's that I didn't read the codes fully but just saw name()
and legacy()
. Thanks for explanation!
I did not, but to work fully it requires the package be installed once for each plugin host so there needs to be some extra stuff happening there to get things going.
FYI, PD needs that functionality as well for its command completions, which are partially sourced from the plugin environment (like this plugin does) and partially from a bundled file containing information about the built-in commands.
For anyone interested in using this plugin, there's now an official video that talks about it and tells you how to install it.
This is also available as a package on Package Control now: CommandsBrowser (github).
This is a simple plugin that allows you to browse all commands provided by plugins (so everything but commands implemented in the core) in a quick panel, which allows you to filter by name. Each entry has a
kind
that indicates if it is anApplicationCommand
,WindowCommand
orTextCommand
, and also provides details on arguments expected and the plugin and module where the command is defined. Choosing a command will open the appropriate file and jump you to the definition of the command.This requires a build of Sublime Text somewhere >= 4080 or so (i.e. it will not work in Sublime Text 3).
The plugin can only detect commands that are running in the same plugin host that it's running in. So to deploy this, you need to install it twice; once into a package with all of the files seen here, and then again in a different package containing only the plugin file.
Choosing the command from the command palette will run the version running in the legacy host, which will gather commands running there and forward them to the command running in the newer host, which will display the whole list.