Sublime Forum

extend_selection.py

#1

Here’s a simple plugin - extend_selection.py - that allows you to use the cursor movement keys to expand the area of selected text without the need to simultaneously hold down the shift key. Create a keybinding for the “toggle_extend_mode” command. For example,{ “keys”: [“ctrl+space”], “command”: “toggle_extend_mode”}. Cancel extend selection mode by pressing the toggle key again. Extend selection mode is also canceled when you type something over selected text, copy the selection, or delete it.

myflag = 1

import sublime, sublime_plugin

class toggle_extend_mode(sublime_plugin.TextCommand):          #Toggle extend_selection on or off
  def run(self, edit):
    global myflag  
    if myflag == 1:
      self.view.set_status('extstatus', "<EXTEND_SELECTION>")
      myflag = 0
    else:
      self.view.set_status('extstatus', "")
      myflag = 1

class KeyboardListener(sublime_plugin.EventListener):           #If extend mode is on, then typing over a selection
  def on_modified(self, view):                                  #will turn it off 
    global myflag
    if myflag == 0:
      myflag = 1
      view.set_status('extstatus', "")
    
class TextCommandEventListener(sublime_plugin.EventListener):   #Inject extend:true into move and move_to commands when extend_mode is on.
  def on_text_command(self, view, command, args):               #Note: Using return command here to change the cursor move parameters will 
    global myflag                                               #cause this Listener to be re-entered. Filter out extend:true to prevent command
    if myflag == 0 :                                            #doubling. Thanks to kingkeith from the sublime text forum for figuring this out!
      if command == "move" and ("extend" not in args or args["extend"] == False):        #move command, extend it.
        return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
      elif command == "move_to" and ("extend" not in args or args["extend"] == False) :  #move_to command, extend it 
        return "move_to", {'extend' : True, 'to' : args['to']}             
      elif command == "left_delete" or command == "right_delete" or command == "copy" :  #copy/delete selected text -> extend_selection off
        myflag = 1  
        view.set_status('extstatus', "")
0 Likes

#2

You, you better have a look at vim :grinning:

Just a quick look in the code, first, when you put a code block, do it this way


```python
import this # my python code
echo "i'm a really messy langage"

so that your code is highlited (working on plenty of website, github for example)

Then, you use `global`, which is not really good, and you should use a different name than `myflag`, it's like `temp`, or `youdontknowwhatthisis` :smile: 

Why do you have to class `EventListners` ? You can put them in the same class, and you should choose a more specific name (so you don't get any conflict with any other plugin)

Also, you should indent your code using 4 spaces (pep 8 I think). Your class name should be in camel case (`MyClassName`), unlike variables and functions/methods who should be in snake case I think it's called (`my_function_name`)

That's a lot of "criticts", but I hope you'll take them the right way, and don't get upset about it :worried: 

So, really, have a look at vim. Maybe you're already using it... 

Matt
0 Likes

#3

Thanks! I don’t mind the criticism at all, since I am a rank amateur and your feedback is really helpful. I understand most of what you are saying. I will put the two EventListeners into the same class. I also get that globals are not looked upon kindly, though at my level of skill it is the easiest way to get the EventListeners to access variables in another class. (I do understand though that it is bad to use ‘myflag’ since that name is like ‘temp’ and could be used anywhere.) I will also make the formatting changes you suggest.

In regard to vim, I’ve used vim before, but I’m not sure I want to use the full vintage mode just to get the ability to toggle extend_selection. Anyway, I will make the changes you suggest - and try to figure out an alternative to those global statements - and then I’ll post up the revision. Thanks again!

3 Likes

#4

So here’s a revision of extend_selection.py. I’ve incorporated the changes suggested by Matt: class variables instead of global variables and the two EventListeners are in a single class. I also made an attempt to follow the naming convention he suggested. (ClassNamesLikeThis and function_names_like_this.) I couldn’t get my keybinding to work reliably with a camel case command name however, so I had to settle for snake case on the first class name. (I.e., “class toggle_extend_mode.”) As for vim: I think Matt’s point is that vim does everything this plugin does and more. If you are using vim you would have no need for this plugin, since you already have visual mode. I agree of course, but vim is a lot more than visual mode - and I’m not looking to add that extra functionality here. (I think this is what Matt was suggesting.) This plugin is just meant to make it a bit easier and quicker to select text with the direction/cursor movement keys. (I’ve also tried to follow Matt’s suggestions about formatting the code that I post here. I’ve indented 4 spaces, but I’m not sure I understand the point about “highlighted” code.)

 import sublime, sublime_plugin

class toggle_extend_mode(sublime_plugin.TextCommand):          
  extend_status_flag = 2   #0 = extend on, 1 = extend off
  def run(self, edit):
    if toggle_extend_mode.extend_status_flag != 0 :
      self.view.set_status('extstatus', "<EXTEND_SELECTION>")
      toggle_extend_mode.extend_status_flag = 0
    else :
      self.view.set_status('extstatus', "")
      toggle_extend_mode.extend_status_flag = 1

class ExtendSelectionToggleEventCapture(sublime_plugin.EventListener):  
  def on_modified_async(self, view):   #New text cancels extend
    if toggle_extend_mode.extend_status_flag == 0:    
        view.set_status('extstatus', "")
        toggle_extend_mode.extend_status_flag = 1

  def on_text_command(self, view, command, args):               
    if toggle_extend_mode.extend_status_flag == 0 :
      if command == "move" and ("extend" not in args or args["extend"] == False) : 
        return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
      elif command == "move_to" and ("extend" not in args or args["extend"] == False) :  
        return "move_to", {'extend' : True, 'to' : args['to']}             
      elif command == "left_delete" or command == "right_delete" or command == "copy" :
        view.set_status('extstatus', "")
        toggle_extend_mode.extend_status_flag = 1
0 Likes

#5

Specifically in regards to indenting 4 spaces, he was referring to PEP 8, and in particular the section on indentation, which recommends 4 spaces per indent level (you’re using 2).

The highlighting part has to do with syntax highlighting when you post code on the forum, I think. If you just indent your code 4 spaces when you post it (or select it and click the Preformatted Text button), it comes out like:

class toggle_extend_mode(sublime_plugin.TextCommand):          
  extend_status_flag = 2   #0 = extend on, 1 = extend off
  def run(self, edit):
    if toggle_extend_mode.extend_status_flag != 0 :
      self.view.set_status('extstatus', "<EXTEND_SELECTION>")
      toggle_extend_mode.extend_status_flag = 0
    else :
      self.view.set_status('extstatus', "")
  toggle_extend_mode.extend_status_flag = 1

If you wrap it like:

```python
my code here
```

It gets syntax highlighted, making it easier to read. In this case you don’t have to indent it (or use that Preformatted text button), which also makes it handier.

class toggle_extend_mode(sublime_plugin.TextCommand):          
  extend_status_flag = 2   #0 = extend on, 1 = extend off
  def run(self, edit):
    if toggle_extend_mode.extend_status_flag != 0 :
      self.view.set_status('extstatus', "<EXTEND_SELECTION>")
      toggle_extend_mode.extend_status_flag = 0
    else :
      self.view.set_status('extstatus', "")
      toggle_extend_mode.extend_status_flag = 1
0 Likes

#6

Can you explain how exactly it wasn’t working reliably? Internally Sublime converts from CamelCase to snake_case, so if your command was named ToggleExtendModeCommand it would be converted by sublime to toggle_extend_mode. Possibly you were using the wrong command name in your binding?

0 Likes

#7

Thanks for explaining the indenting and highlighting - now I get it! As for the keybind, it looks like I did that wrong too!! I had:
{ “keys”: [“ctrl+space”], “command”: “ToggleExtendMode”},
But “ToggleExtendMode” is supposed to be “ToggleExtendModeCommand”?

0 Likes

#8

The following command:

class ToggleExtendModeCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        pass

gets converted to the command name toggle_extended_mode. Basically it throws away the Command on the end and then converts all uppercase characters to lowercase and prefixes them with an underscore (except the first one). I believe that the Command part is optional and the conversion will still work, but it’s considered good coding practice to use the Command suffix on the class name.

1 Like

#9

:smile: again, post your code (here in json) using the ` symbol. Like this:

```json
{ "keys": ["ctrl+space"], "command": "ToggleExtendMode"}
```

so you get

{ "keys": ["ctrl+space"], "command": "ToggleExtendMode"}

Confirmed.

@daneli some other conventions/advice :smile: :

is for the cool kids, == for the rest [convention]

When you compare a value to a boolean (True, False) or None, use the operator is instead of ==, and is not instead of !=.

Your flag

You said it yourself, this is a flag, and as you see, it has only 2 possible values (1, or 0). So, you could use some booleans.

Then your name can be improved. You put a comment, but always remember

Perfect developers don’t need to write comments

It doesn’t mean that we shouldn’t write comments, but you should avoid it.

Good code writing workflow (at least what I consider being a good one)
  • think, sketch on a bit of paper if needed
  • write as clearly as possible (Programs must be written for people to read, and only incidentally for machines to execute. – Harold Abelson)
  • if it’s not clear enough, then write comment

So, say your flag name would be is_extend_selection_active, or extending_selection? It would speak for itself, wouldn’t it? (if you’re using boolean, it would even more). So in this case, you don’t need comment. :slightly_smiling:

import should be on several line (with a secret sublime tip :wink:) [convention]

When you use import, you should write one import for each “library” you import, instead of using commas. Like so

# no
import sublime, sublime_plugin

# yes
import sublime
import sublime_plugin

More typing? Yes, but it’s cleaner, and in this case, I’m going to give you a tip so that you have to press even less keys! But it secret, so shhh (:smile: just kidding)

Type import sublime, press ctrl+shift+d (on windows, otherwise, go have a look at what it is here: Edit -> Line -> Duplicate Line) and now, you just have to type _plugin.

White spaces

You have some white spaces at the end of the line. It’s useless, and a bit annoying sometimes. There is a native option that removes it for you, but this plugin does the job better and stays really simple.

the fabulous operator [applause] in

When you do this: command == "left_delete" or command == "right_delete" or command == "copy". Picture this: you have 100 different possibilities. What would you do. Copy/paste? Here’s the good solution.

if command in ['left_delete', 'right_delete', 'copy']:
    # do your stuff

Comments

I’m not sure if it’s a convention, but it looks for me cleaner when you add a space after your #.


#no

#stin?ky comment

# yes

# Beautiful is better than ugly.

It’s up to you to do this, but this part is more an excuse to give you an other tip (maybe you already knew that one): ctrl+/ (un)comments the current line if there is not selection, otherwise, it toggles the selection (try it).

Colons

There shouldn’t be any space before a :.


#no
if something == True :
    print ("something's wrong, I can feel it -- Eminem")

# yes
if something is True:
    print("a good looking code")

I don’t think you understood what I meant. Look:


# indent code using **2** spaces

class MySuperCommandCommand(sublime.TextCommand):

  def run(self, edit):
    print('You noticed my command name. It can be called from key binding for example like this my_super_command')
    print('As @OdatNurd explained, the `Command` at the end is automatically striped')
    print("Here, I didn't have a choice about adding the conventional `Command`")
    print(", because I would have had to call my command `my_super` otherwise")
    print("If you have any trouble with this, I've a made a plugin that allows you to transform you command name")
    print("easily: https://github.com/math2001/st-user-package/blob/master/TransformCommandName.py")

# indent code using **4** spaces

class MySuperCommandCommand(sublime.TextCommand):

    def run(self, edit):
        print('You noticed my command name. It can be called from key binding for example like this my_super_command')
        print('As @OdatNurd explained, the `Command` at the end is automatically striped')
        print("Here, I didn't have a choice about adding the conventional `Command`")
        print(", because I would have had to call my command `my_super` otherwise")
        print("If you have any trouble with this, I've a made a plugin that allows you to transform you command name")
        print("easily: https://github.com/math2001/st-user-package/blob/master/TransformCommandName.py")

After this, your code should look pythonic (awesome :smile:)

TL;DR: please read it :yum:

BTW: open the sublime console, and type: import this. It’s the 20 aphorisms guiding principles for Python’s design. Read them, and count them (:wink:) a bit more info

A big message again :laughing:

Matt

0 Likes

#10

So, I have a structurational suggestion: if you assign a setting value to a view, the potential inconsistencies of sharing the same flag amongst all views is easily worked around and you could even directly define key bindings with a context specification instead of implementing an event listener. You would do it like this: view.settings().set("selection_mode", True).

I would link more references but I cba when on mobile.

3 Likes

#11

Matt, OdatNurd and FictheFoll: Thank you for your feedback - it is very helpful.

OdatNurd: It seems I needed to restart sublime to get my new keybindings to take. Failing to do that is apparently why changing the command name to CamelCase didn’t work with my new keybinding.

OdatNurd and Matt: I think I really do finally get how to format the code blocks with proper indents and for posting here. I don’t know if it is as vivid of Dorothy’s arrival in the Land of Oz, but I see the colors now.

Matt: “TL;DR: please read it.” Since you took the time to write it, the least I can do is read it! Good point that a truly descriptive name for a boolean flag makes the meaning of the code clearer with much less need for a comment. I think I worry that being too spare with comments introduces undesirable ambiguity, but I gather the ‘pythonic’ way is make the code better and more clear to begin with. Also, thanks for the suggestions about white space, colons, import statements, and “in”. The Zen of Python and the concept of “pythonic” were new to me. The point then is that it isn’t just about making the code “work”, or even listening to the accumulated experience of a bunch of programmers recommending the most efficient way of doing things. In addition, a community (with its own aesthetic, or even ‘culture’) develops around the concept of a “proper way of doing things.” Asserting and maintaining that idea helps sustain this community. In a broader world where the “right way of doing things” seems to be increasingly threatened, it is reassuring to find this idea alive and well here!

FichteFoll: I do see how a setting value could replace the flag and how keybindings could replace the on_text_command EventListener. I don’t understand though how to avoid the need for the on_modified EventListener. If I want to cancel selection_mode when the user enters new text over a selection, do I not need on_modified? Maybe though I am not understanding your suggestion?

import sublime
import sublime_plugin

class ToggleExtendMode(sublime_plugin.TextCommand):
    extend_selection_mode_on = False
    def run(self, edit):
        if not ToggleExtendMode.extend_selection_mode_on:
            self.view.set_status('extend_mode_status_bar_msg', "<EXTEND_SELECTION>")
            ToggleExtendMode.extend_selection_mode_on = True
        else:
            self.view.set_status('extend_mode_status_bar_msg', "")
            ToggleExtendMode.extend_selection_mode_on = False

class ExtendSelectionToggleEventCapture(sublime_plugin.EventListener):
    def on_modified(self, view):  # Typing over a selection cancels extend
        if ToggleExtendMode.extend_selection_mode_on:
            view.set_status('extend_mode_status_bar_msg', "")
            ToggleExtendMode.extend_selection_mode_on = False

    def on_text_command(self, view, command, args):
        if ToggleExtendMode.extend_selection_mode_on:
            if command == "move" and ("extend" not in args or args["extend"] is False):
                return "move", {'forward' : args['forward'], 'by': args['by'], 'extend': True}
            elif command == "move_to" and ("extend" not in args or args["extend"] is False):
                return "move_to", {'extend' : True, 'to' : args['to']}
            elif command in ["left_delete", "right_delete", "copy"]:
                view.set_status('extend_mode_status_bar_msg', "")
                ToggleExtendMode.extend_selection_mode_on = False
0 Likes

#12

Ah sorry, I specifically meant the on_text_command hook with that. With the setting set, you would then define key keybindings that are only active while that setting’s value is true. This is also how the Vintage package works.

0 Likes