Sublime Forum

Access / Modify Keymap and Commands Dynamically?

#1

Background Data:

I am developing a package which has a configuration item that is a LIST of strings. Every time this list changes, it also has to be changed in 2 hard-coded regular expressions that are part of CONTEXT dictionary list for a COMMAND and a KEY MAP entry.

Reason: It is intended that the end user be able to modify and/or add to that list, and to make it simpler (and more maintainable), I want to make it so that the LIST becomes the sole source, and the 2 RegExes get updated when the package gets initialized.

Example:

  1. Given this configurable list: ["/*", “//”, “#”]
  2. internally build this RegEx: (/\*|//|#), then
  3. insert that regular expression as part of a dictionary into the CONTEXT list of one COMMAND and one KEY-MAPPED COMMAND, e.g.
{ "key": "preceding_text", "operator": "regex_contains", "operand": "^\\s*(/\\*|//|#)(.[ht]|.)?$", "match_all": true },

Question:

Is it possible to get access to a keymap entry as well as a COMMAND to make that modification dynamically (e.g. during package initialization)? If so, where should I look to find how to access these structures in memory?

Partial Info

Note: I have some code from a utility that makes a list of global commands, so I see how that is iterated through using these top-level variables in sublime_plugin.py:

  • application_command_classes = []
  • window_command_classes = []
  • text_command_classes = []

So I am hopeful this leads to an answer for the COMMAND. However, I have never seen anything that allows plugin access to the global keymap, and specifically, the ability to modify the “context” list of dictionaries for a command as described above. Is there a way to do it?

0 Likes

#2

In memory, no, but sublime.find_resources('*.sublime-keymap') will give you the list of all of the keymap files (though it sounds like you probably know what one you want) and sublime.load_resource() will allow you to load the file into memory, where you can use sublime.decode_value() to turn it into an object. From there you could modify it and write it back.

This however is not recommended unless you’re making a file that contains literally only those key bindings, because this round trip can blow away things like comments or commented out entries that the user might expect to remain.

For what you want, you probably want to investigate the on_query_context event; that would let you create your own custom context, which would not need to have the regex specifed in the sublime-keymap file, it could just reference it internally.

That way the binding Just Works™ without you needing to do anytihng special at all.

Based on what you mentioned above as your goal:

  1. Have the list be in a sublime-settings file for your package; sublime.load_settings() will give you a settings object. On plugin init, load your config, and compile the regex.
  2. The settings object has an add_on_change() and clear_on_change() that you can use to be told when your settings file changes; from that you can get told when the user modified the settings file, so you can recompile the regex (remember to remove the listener on plugin unload!)
  3. You implement EventListener.on_query_context or ViewEventListener.on_query_context and define a custom context name that returns True or False accordingly, depending on the regex that it currently know about
  4. Use that custom context name in the keymap instead of the built in one you’re using now; it just needs to have the name and any other parts you might want to include (probably just match_all) but it won’t need the regex because it will have access to it internally.

This might help as far as the context goes; I can’t remember offhand if I have a video for Settings objects (if you need that) but if so I can check tomorrow; it’s kinda late at the moment.

1 Like

#3

Good morning, Terence ( @OdatNurd )! Indeed that sounds like EXACTLY what I needed – to be able to move the evaluation of that regex internally (in the plugin), so that it can be modified when it changes (as well as when the package gets initialized).

Indeed, I already have the building and compiling of the RegEx in place for when the package is initialized, and an is_enabled() event hook on the command itself (so part way there), but the notification (event) on when the configuration changes, as well as the on_query_context hook are the parts I was missing. (And yes, I appreciate the differences between is_enabled() and on_query_context() and will be factoring out my “context-is-valid test” to be used by both of them.)

I’ll be back in touch either with more questions, or to confirm it is working!

Terence, you’re a blessing to the Sublime Text community!

Happy New Year!
Kind regards,
Vic

0 Likes

#4

Update:

Good evening, Terence ( @OdatNurd )!

As promised, I’m reporting. Everything is working as expected!

  • I have factored out the routine that builds and compiles my RegEx from that list.
  • In the package init() I have used add_on_change() settings-object method to hook when my package settings have been updated, which in turn, re-builds and re-compiles the RegEx.
    pc_setting.obj = sublime.load_settings("ProComment.sublime-settings")
    pc_setting.obj.add_on_change('', _build_and_compile_specifier_line_regex)
    
    which successfully “hooks” that event (when package settings change).
  • I added a ViewEventListener to capture my new (now-stable) custom context name so that mapped keys are processed correctly, and
  • this same logic (factored out into its own routine) now also determines the result of the Command’s is_enabled() method.

And it all appears to work fabulously!

One point of concern I have is in sublime::Settings::add_on_change() method, in the sublime.py file it “type hints” the callback argument as type Callable[[], None], and I’m not entirely sure how to read that – if I should be accepting a list as an argument. Attempts to do so to see what came through in the argument resulted in an error message TypeError: _build_and_compile_specifier_line_regex() missing 1 required positional argument: \'arg_name\' when I change the package settings, so I am currently assuming NO ARGUMENTS for that function is okay.

I haven’t yet become well versed in Python type hinting syntax to understand that callback argument as type Callable[[], None]. Do you understand it? If so, what does it mean?

Looking forward to your reply,
Vic

0 Likes

#5

Callable[[], None] breaks down to "Something that is callable whose argument list is an empty list (i.e. no arguments) and which returns None (i.e. no return value).

So what you’re doing there with not using an argument is indeed correct. The settings listener callback is just a function that gets invoked any time the settings file is reloaded (which includes settings changes as well as just the file being saved even without modifications).

Note that your code example is using an empty key for the add_on_change handler, which works but is not optimal.

You also want to make sure that you remove the listener when your plugin unloads, or things will get steadily worse for you.

For example, you can try running the following plugin and then comment out the call to plugin_unloaded() and then save the file a few times to make it reload, and then modify your preferences file and see what the console looks like.

import sublime
import sublime_plugin


def do_a_thing_with_settings():
  """
  A function that does something with settings; code here would load the
  settings file, or otherwise access some sort of globally cached one, and then
  do something with said setting. Here we are just printing the value of the
  setting out.

  NOTE: As a settings listener target, this gets invoked any time any setting
        changes, so part of your logic here (if it matters) is to check and see
        if the setting you care about is the one that changed. Usually does not
        matter though.
  """
  settings = sublime.load_settings("Preferences.sublime-settings")
  print(f"The font size is {settings.get('font_size', 10)}")


def plugin_loaded():
  """
  This gets executed once after your plugin is loaded and the API is fully
  ready to go. This is a good place to initialize things like a settings file
  listener, for example. */
  """
  # Load the settings file; every time you invoke this with the same settings
  # file, you get the same object back (so you can cache this settings file or
  # just reload as you need)
  settings = sublime.load_settings("Preferences.sublime-settings")

  # Indicate an interest in when settings change; the key here is important;
  # give it something unique to you (maybe namespace with your plugin name);
  # this is needed to remove the listener.
  settings.add_on_change('_test_listener', do_a_thing_with_settings);

  # Do now with the settings what you would do with them whenever the settings
  # file changes.
  do_a_thing_with_settings()


def plugin_unloaded():
  """
  This gets executed when your plugin is unloaded, which happens when you save
  it and it reloads, and also when the package that it's in is disabled. Here
  is a good place to make sure that you don't leak the settings listener.
  """
  # Get the settings object and then clear the listener away; this is done via
  # the API and the key that you used to register the listener.
  settings = sublime.load_settings("Preferences.sublime-settings")
  settings.clear_on_change('_test_listener')

1 Like

#6

Thank you, Terence ( @OdatNurd ) for letting me pick your brain.

I was erroneously calling the plugin_loaded() event hook “initialization” because my plugin_loaded() looks like this:

def plugin_loaded():
    init()

which loads my package’s settings, initializes the package, and adds the add_on_change() event hook (callback) shown above. I only showed a couple of lines of code from it above. And thank you for the “heads-up” re the need for clear_on_change() on package unload with the matching tag name! I much appreciate your thoroughness!

Kind regards,
Vic

1 Like