sublimehq / sublime_text

Issue tracker for Sublime Text
https://www.sublimetext.com
803 stars 39 forks source link

CamelCase converter in view.run_command is broken with respect to consecutive uppercase characters #6447

Open mulle-nat opened 1 month ago

mulle-nat commented 1 month ago

Description of the bug

I wanted to write this:

import sublime
import sublime_plugin
import re

class ObjectiveCSelectorHighlighterCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        print("was called")

class ObjectiveCSelectorListener(sublime_plugin.EventListener):
    def on_activated(self, view):
        print("will be called")
        view.run_command("objective_c_selector_highlighter")

but this is not possible, because the camel case conversion silently fails. I tried to figure out, if it should be in some other format but to no avail:

class XXCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        print("XX was called")

class XXListener(sublime_plugin.EventListener):
    def on_activated(self, view):
        print("XX will be called")
        view.run_command("xx")
        view.run_command("_xx")
        view.run_command("xx_")
        view.run_command("_xx_")
        view.run_command("x_x")
        view.run_command("_x_x")
        view.run_command("x_x_")
        view.run_command("_x_x_")
        view.run_command("xX")
        view.run_command("_xX")
        view.run_command("xX_")
        view.run_command("_xX_")
        view.run_command("x_X")
        view.run_command("_x_X")
        view.run_command("x_X_")
        view.run_command("_x_X_")
        view.run_command("XX")
        view.run_command("_XX")
        view.run_command("XX_")
        view.run_command("_XX_")
        view.run_command("X_X")
        view.run_command("_X_X")
        view.run_command("X_X_")
        view.run_command("_X_X_")
        view.run_command("Xx")
        view.run_command("_Xx")
        view.run_command("Xx_")
        view.run_command("_Xx_")
        view.run_command("X_x")
        view.run_command("_X_x")
        view.run_command("X_x_")
        view.run_command("_X_x_")

This has wasted me a lot of time, as I couldn't figure out, why my commands never ran. Some error message or even better a fix would improve things.,

Steps to reproduce

Create a plugin, drop in the code. Open the console, see whats been printed, when you are switching buffers.

Expected behavior

Things work.

Actual behavior

Things do not work.

Sublime Text build number

4169

Operating system & version

Ubuntu 22

(Linux) Desktop environment and/or window manager

Gnome

Additional information

No response

OpenGL context information

No response

jfcherng commented 1 month ago

lol... the command name becomes x_xCommand in your case. I thought trailing Command will be striped first but it looks like it's not.

jwortmann commented 1 month ago

See the default name implementation from the Command class (in sublime_plugin.py):

def name(self) -> str:
        """
        Return the name of the command. By default this is derived from the name
        of the class.
        """
        clsname = self.__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

So it looks like the underscore & lowercasing is not applied to multiple consecutive uppercase letters. And as a result _command will not be striped from the name if there is an uppercase letter before the Command (in your second example).

Note that the name() method can simply be overridden if desired.

mulle-nat commented 1 month ago

The function would be much better, if it was written like this:

    def name(self) -> str:
        clsname = self.__class__.__name__
        if clsname.endswith("Command"):
            clsname = clsname[0:-7]
        name = clsname[0].lower()
        last_upper = False
        for c in clsname[1:]:
            if c.isupper() and not last_upper:
                name += '_'
            name += c.lower()
            last_upper = c.isupper()
        return name

This ensures that "Command" is properly stripped no matter what and that all the characters are lowercase no matter what. It will produce "highlight_objectivec_selector" instead of "highlight_objective_c_selector", but that's still better, than what it did before.