Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger an action for every alphanumeric keypress

I have the following lines in a plugin's Default (Windows).sublime-keymap file:

...
{ "keys": ["ctrl+shift+a"], "command": "table_editor_align", "context":
    [
        { "key": "setting.enable_table_editor", "operator": "equal", "operand": true, "match_all": true },
        { "key": "preceding_text", "operator": "regex_contains", "operand": "^\\s*\\|", "match_all": true },
        { "key": "following_text", "operator": "regex_contains", "operand": "$", "match_all": true }
    ]
},
...

Instead of triggering this command only when ctrl+shift+a, I'd like to trigger this command after every alphanumeric keypress (A-Z, a-z, 0-9, and why not also accents é, à, ç, etc. i.e. all characters that we use while writing)?

"keys": ["[a-zA-Z0-9_]"]

doesn't seem to work.

Note: the plugin is currently a subclass of sublime_plugin.TextCommand, and I think keeping this is mandatory for it to work. The plugin I'm trying to modify is https://github.com/vkocubinsky/SublimeTableEditor, I would like that auto-re-align is made automatically after each keypress, rather than after each CTRL+SHIFT+A like here:

enter image description here

like image 208
Basj Avatar asked Jan 15 '17 14:01

Basj


People also ask

How to trigger a keypress event manually?

To trigger the event manually, apply .keypress () without an argument: After this code executes, clicks on the Trigger the handler div will also log the message. If key presses anywhere need to be caught (for example, to implement global shortcut keys on a page), it is useful to attach this behavior to the document object.

What are the events related to keypresses?

The events related to keypresses are as follows : 1 keydown: This event is triggered when a key is pressed down. 2 keypress: This event is triggered when a key is pressed. This event fails to recognise keys such as tab, shift, ctrl, backspace etc. 3 keyup: This event is triggered when a key is released.

What is the onkeypress event?

The onkeypress event occurs when the user presses a key (on the keyboard). Note: The onkeypress event is not fired for all keys (e.g. ALT, CTRL, SHIFT, ESC) in all browsers. To detect only whether the user has pressed a key, use the onkeydown event instead, because it works for all keys.

What is keypress event in access form?

Form.KeyPress event (Access) The KeyPress event occurs when the user presses and releases a key or key combination that corresponds to an ANSI code while a form or control has the focus. This event also occurs if you send an ANSI keystroke to a form or control by using the SendKeys action in a macro or the SendKeys statement in Visual Basic.


1 Answers

There is no way that I know of to bind a key to a large class of keys at once, so in order to do what you want, you would have to create an individual mapping for each of the keys one at a time, which is rather unwieldy and quite likely not a good idea.


[edit] There is actually a way to do this; I've added more information to the end of my answer [/edit]


From some simple testing it would appear that the keys that you're interested in (that input text) are directly handled by the Sublime core unless you provide a mapping for them. That is to say, they subvert the normal command mechanism, meaning that you can't just catch whenever text is being inserted and reformat.

One way to go about this is, as suggested by MattDMO, to use an on_modified handler to track when the buffer is being modified and then trigger the realignment that way.

An example plugin that can do something like that is the following:

import sublime
import sublime_plugin
import re

class TableFormatEventListener(sublime_plugin.EventListener):
    def __init__(self):
        self._views = dict()
        self.regex = re.compile(r"\s*\|")

    def on_modified(self, view):
        # Only for views with table editing enabled that are not already being
        # modified
        if (view.settings().get("enable_table_editor", False) and
                self._views.get(view.id(), False) is False):

            for s in view.sel():
                line = view.substr(view.line(s.begin()))
                prior = view.substr(s.begin() - 1) if s.begin() > 0 else ""

                # Only if all cursors are inside of a table row and the
                # character prior to the cursor is not a space
                if self.regex.match(line) is None or prior == " ":
                    return

            # Perform the realignment
            self._views[view.id()] = True
            view.run_command("table_editor_align")
            self._views[view.id()] = False

    def on_text_command(self, view, cmd, args):
        # Don't trigger reformatting while an undo is happening
        if cmd == "undo":
            self._views[view.id()] = True

    def on_post_text_command(self, view, cmd, args):
        # Undo is complete; resume reformat handling
        if cmd == "undo":
            self._views[view.id()] = False

This implements an event listener that triggers every time a view is modified. This means that text was added or deleted, and also covers things such as pastes (literally anything that modifies the buffer; more on that in a second).

The plugin does the same sort of context checking that the key binding does, to ensure that all of the available carets are currently inside of a table line, as if you invoke the command to realign the table otherwise, it throws an error.

Additionally, a check is also done to see if the last character added was a space. This is because the reformat seems to remove trailing spaces, which effectively blocks you from being able to enter a space anywhere in a table.

Assuming that all cursors are in table lines and did not just insert a space, the command to realign the table is executed on the current view.

The thing to watch for here is that realigning the table causes the contents of the buffer to be modified, which then triggers the plugin listener again, which realigns the table again, over and over in a loop until the plugin host crashes.

To stop that from happening, we keep a track of the views that should have their on_modified event ignored, adding the current view to the list prior to modifying the table and then removing it once we're done.

A side effect of what the plugin does is that any change you make inside of a table results in two modifications; the change that you made, and the change that realigns the table (even if it does not change). This means that in order to undo changes in the table, you sometimes have to press Undo more times than you might think.

This causes potential issues. For example, if you were to backspace at the start of a table row, that row and the previous row would be on the same line. The modification calls the realignment function, which formally makes the two rows one.

Now you're in a bit of a pickle because you can't undo the change; when you press undo once, it undoes the realignment, but in doing so it modifies the buffer, which triggers the realignment to immediately happen again.

In order to solve that problem, the event listener also listens for when an undo command is about to happen, and makes sure that while the undo is happening, the modification handler will not realign the table.

As I don't use this particular plugin, there may be similar edge cases for other scenarios that would need to be handled in this manner.

Performance may or may not suffer depending on how large a table you happen to be editing. In my (extremely simplistic) testing, the performance hit on small tables was negligible on my machine. Your mileage may vary, so it may potentially be a good idea to have extra checking in the code that stops it from triggering if the current file is over some line threshold or something along those lines.


[edit]

As it happens, there actually is a way to do something similar to what you originally asked. It is possible to assign a key binding that will trigger for any inserted character, although you still need a bit of glue plugin code to tie everything together (and a couple of usability issues remain pertaining to how the table plugin is doing the reformat).

The plugin code required is a command that will accept a character and insert the text as it normally would, but then also trigger the table alignment command.

import sublime
import sublime_plugin

class TextAndAlignTableCommand(sublime_plugin.TextCommand):
    def run(self, edit, character):
        self.view.run_command("insert", {"characters": character})
        if character != " ":
            self.view.run_command("table_editor_align")

This defines a command that takes a single argument of character, which it will insert into the buffer the way a character would normally be entered. Then, if the character is not a space, it also invokes the command to realign the table.

With this in place, the key binding to create is:

{ "keys": ["<character>"], "command": "text_and_align_table", "context":
    [
        { "key": "setting.enable_table_editor", "operator": "equal", "operand": true, "match_all": true },
        { "key": "preceding_text", "operator": "regex_contains", "operand": "^\\s*\\|", "match_all": true },
        { "key": "following_text", "operator": "regex_match", "operand": "\\s*\\|.*$", "match_all": true }
    ]
},

This is basically the key binding that you mentioned above (which is the default for the underlying table plugin to trigger the reformat) with some slight modifications.

Firstly, the key is bound to the key <character>, which makes it potentially trigger for any character that would otherwise be inserted into the buffer.

Secondly, it uses our custom command from above which will first insert the text and then reformat the table.

Thirdly, the following_text context is modified so that it only triggers when it is at the end of a column in the table, so that it is possible to insert text into the middle of a column without having the cursor position jumped to the end of the column.

When bound this way, the binding will trigger for any single character, and will invoke the command provided with an argument that tells you what the character was. The binding itself can't have a modifier on it (e.g. ctrl, shift, etc), but the character you get is the one that would have otherwise been typed.

This is a lot cleaner and doesn't have the same problem as the code above w/regards to undo because all of the actions taken are part of the same edit operation.

Additionally, having the binding not trigger while inside of a column body removes the issue with being able to insert text in the middle of the column since the table realignment shifts the cursor location. This does leave the table unaligned, but the existing key binding can be used in this situation to rectify the issue.

On the other hand, it is still impossible to have spaces at the end of a column value (after a reformat) because the table reformat wants to remove them. To stop that from happening the underlying plugin would have to be modified to not do that, but that seems like it would compromise the table formatting somewhat.

like image 83
OdatNurd Avatar answered Oct 03 '22 13:10

OdatNurd